closure_tree 4.5.0 → 4.6.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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +11 -3
- data/CHANGELOG.md +13 -0
- data/Gemfile +13 -0
- data/README.md +61 -12
- data/closure_tree.gemspec +4 -5
- data/gemfiles/activerecord_3.2.gemfile +12 -0
- data/gemfiles/activerecord_4.0.gemfile +12 -0
- data/gemfiles/activerecord_4.1.gemfile +12 -0
- data/gemfiles/activerecord_edge.gemfile +12 -0
- data/lib/closure_tree/acts_as_tree.rb +2 -9
- data/lib/closure_tree/deterministic_ordering.rb +4 -0
- data/lib/closure_tree/finders.rb +4 -4
- data/lib/closure_tree/hash_tree.rb +1 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +19 -6
- data/lib/closure_tree/model.rb +2 -10
- data/lib/closure_tree/numeric_deterministic_ordering.rb +53 -36
- data/lib/closure_tree/numeric_order_support.rb +20 -13
- data/lib/closure_tree/support.rb +8 -0
- data/lib/closure_tree/support_attributes.rb +10 -0
- data/lib/closure_tree/test/matcher.rb +85 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/closure_tree.rb +14 -1
- data/spec/cache_invalidation_spec.rb +39 -0
- data/spec/{support → db}/models.rb +6 -0
- data/spec/db/schema.rb +18 -0
- data/spec/label_spec.rb +171 -46
- data/spec/matcher_spec.rb +32 -0
- data/spec/parallel_spec.rb +85 -49
- data/spec/spec_helper.rb +6 -96
- data/spec/support/database.rb +49 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/deprecated/attr_accessible.rb +5 -0
- data/spec/support/hash_monkey_patch.rb +13 -0
- data/spec/support/helpers.rb +8 -0
- data/spec/support/sqlite3_with_advisory_lock.rb +10 -0
- data/tests.sh +7 -2
- metadata +31 -43
- data/spec/parallel_prepend_sibling_spec.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74e74ddf4f763041d38464015bd608db38833721
|
4
|
+
data.tar.gz: 91aeb3af057e67f9b97576ef7cdd2bce155572dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 298110a1706950f53e77ef1e024fdce605ba0fdd4b0753980b4287c757bcf93cc7086cd548b5ee395a47a905be07ad79b8ccc7c41d66a860cd3e45d4f39f9990
|
7
|
+
data.tar.gz: cd8c0c61c24388929dcefd9611090b6ba0bbc4343f284298b8ae22593d4a1830482ebe8e635362da6f790b246584029e1b3501a7d50d86e13a7e4f61b22243b4
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--format documentation --color --order random
|
data/.travis.yml
CHANGED
@@ -2,14 +2,17 @@ language: ruby
|
|
2
2
|
rvm:
|
3
3
|
- 1.9.3
|
4
4
|
- 2.1.2
|
5
|
-
-
|
6
|
-
|
5
|
+
- rbx-2
|
6
|
+
- jruby-19mode
|
7
|
+
# - ruby-head
|
8
|
+
# - rbx-head
|
9
|
+
# - jruby-head
|
7
10
|
|
8
11
|
gemfile:
|
9
12
|
- gemfiles/activerecord_3.2.gemfile
|
10
13
|
- gemfiles/activerecord_4.0.gemfile
|
11
14
|
- gemfiles/activerecord_4.1.gemfile
|
12
|
-
- gemfiles/activerecord_edge.gemfile
|
15
|
+
# - gemfiles/activerecord_edge.gemfile
|
13
16
|
|
14
17
|
env:
|
15
18
|
- DB=sqlite
|
@@ -22,7 +25,12 @@ matrix:
|
|
22
25
|
fast_finish: true
|
23
26
|
allow_failures:
|
24
27
|
- gemfile: gemfiles/activerecord_edge.gemfile
|
28
|
+
- rvm: rbx-2
|
29
|
+
- rvm: jruby-19mode
|
25
30
|
- rvm: ruby-head
|
31
|
+
- rvm: rbx-head
|
32
|
+
- rvm: jruby-head
|
33
|
+
|
26
34
|
exclude:
|
27
35
|
- rvm: ruby-head
|
28
36
|
gemfile: gemfiles/activerecord_3.2.gemfile
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
### 4.6.0
|
4
|
+
|
5
|
+
* Deterministically ordered trees are guaranteed to have a sort_order now.
|
6
|
+
|
7
|
+
**This may be a breaking change if you're expecting sort_order to be nullable.**
|
8
|
+
|
9
|
+
Many thanks to [David Schmidt](https://github.com/inetdavid) for raising and
|
10
|
+
working on the issue!
|
11
|
+
|
12
|
+
* Added ```append_child``` and ```prepend_child```
|
13
|
+
|
14
|
+
* All raw SQL is now ```strip_heredoc```'ed
|
15
|
+
|
3
16
|
### 4.5.0
|
4
17
|
|
5
18
|
* Merged a bunch of great changes from [Abdelkader Boudih](https://github.com/seuros),
|
data/Gemfile
CHANGED
@@ -1,3 +1,16 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
gem 'foreigner', :git => 'https://github.com/mceachen/foreigner.git'
|
3
|
+
|
4
|
+
platforms :ruby, :rbx do
|
5
|
+
gem 'mysql2'
|
6
|
+
gem 'pg'
|
7
|
+
gem 'sqlite3'
|
8
|
+
end
|
9
|
+
|
10
|
+
platforms :jruby do
|
11
|
+
gem 'activerecord-jdbcmysql-adapter'
|
12
|
+
gem 'activerecord-jdbcpostgresql-adapter'
|
13
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
14
|
+
end
|
15
|
+
|
3
16
|
gemspec
|
data/README.md
CHANGED
@@ -26,6 +26,7 @@ closure_tree has some great features:
|
|
26
26
|
* 2 SQL INSERTs on node creation
|
27
27
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
28
28
|
* __Support for Rails 3.2, 4.0, and 4.1__
|
29
|
+
* __Support for Ruby 1.9 and 2.1 (jRuby and Rubinius are still in development)__
|
29
30
|
* Support for reparenting children (and all their descendants)
|
30
31
|
* Support for [concurrency](#concurrency) (using [with_advisory_lock](https://github.com/mceachen/with_advisory_lock))
|
31
32
|
* Support for polymorphism [STI](#sti) within the hierarchy
|
@@ -376,6 +377,15 @@ If your ```order``` column is an integer attribute, you'll also have these:
|
|
376
377
|
* ```node1.self_and_descendants_preordered``` which will return descendants,
|
377
378
|
[pre-ordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order).
|
378
379
|
|
380
|
+
* ```node1.append_child(node2)``` (which is an alias to ```add_child```), which will
|
381
|
+
1. set ```node2```'s parent to ```node1```
|
382
|
+
2. set ```node2```'s sort order to place node2 last in the ```children``` array
|
383
|
+
|
384
|
+
* ```node1.prepend_child(node2)``` which will
|
385
|
+
1. set ```node2```'s parent to ```node1```
|
386
|
+
2. set ```node2```'s sort order to place node2 first in the ```children``` array
|
387
|
+
Note that all of ```node1```'s children's sort_orders will be incremented
|
388
|
+
|
379
389
|
* ```node1.prepend_sibling(node2)``` which will
|
380
390
|
1. set ```node2``` to the same parent as ```node1```,
|
381
391
|
2. set ```node2```'s order column to 1 less than ```node1```'s value, and
|
@@ -388,27 +398,27 @@ If your ```order``` column is an integer attribute, you'll also have these:
|
|
388
398
|
|
389
399
|
```ruby
|
390
400
|
|
391
|
-
root = OrderedTag.create(:
|
392
|
-
a =
|
393
|
-
b = OrderedTag.create(:
|
394
|
-
c = OrderedTag.create(:
|
401
|
+
root = OrderedTag.create(name: 'root')
|
402
|
+
a = root.append_child(Label.new(name: 'a'))
|
403
|
+
b = OrderedTag.create(name: 'b')
|
404
|
+
c = OrderedTag.create(name: 'c')
|
395
405
|
|
396
406
|
# We have to call 'root.reload.children' because root won't be in sync with the database otherwise:
|
397
407
|
|
398
408
|
a.append_sibling(b)
|
399
|
-
root.reload.children.
|
409
|
+
root.reload.children.pluck(:name)
|
400
410
|
=> ["a", "b"]
|
401
411
|
|
402
412
|
a.prepend_sibling(b)
|
403
|
-
root.reload.children.
|
413
|
+
root.reload.children.pluck(:name)
|
404
414
|
=> ["b", "a"]
|
405
415
|
|
406
416
|
a.append_sibling(c)
|
407
|
-
root.reload.children.
|
417
|
+
root.reload.children.pluck(:name)
|
408
418
|
=> ["b", "a", "c"]
|
409
419
|
|
410
420
|
b.append_sibling(c)
|
411
|
-
root.reload.children.
|
421
|
+
root.reload.children.pluck(:name)
|
412
422
|
=> ["b", "c", "a"]
|
413
423
|
```
|
414
424
|
|
@@ -437,6 +447,10 @@ database with multiple threads, and don't provide an alternative mutex.
|
|
437
447
|
|
438
448
|
## FAQ
|
439
449
|
|
450
|
+
### Are there any how-to articles on how to use this gem?
|
451
|
+
|
452
|
+
Yup! [Ilya Bodrov](https://github.com/bodrovis) wrote [Nested Comments with Rails](http://www.sitepoint.com/nested-comments-rails/).
|
453
|
+
|
440
454
|
### Does this work well with ```#default_scope```?
|
441
455
|
|
442
456
|
No. Please see [issue 86](https://github.com/mceachen/closure_tree/issues/86) for details.
|
@@ -484,24 +498,59 @@ after do
|
|
484
498
|
end
|
485
499
|
```
|
486
500
|
|
501
|
+
## Testing with Closure Tree
|
502
|
+
|
503
|
+
Closure tree comes with some RSpec2/3 matchers which you may use for your tests:
|
504
|
+
|
505
|
+
```ruby
|
506
|
+
require 'spec_helper'
|
507
|
+
require 'closure_tree/test/matcher'
|
508
|
+
|
509
|
+
describe Category do
|
510
|
+
# Should syntax
|
511
|
+
it { should be_a_closure_tree }
|
512
|
+
# Expect syntax
|
513
|
+
it { is_expected.to be_a_closure_tree }
|
514
|
+
end
|
515
|
+
|
516
|
+
describe Label do
|
517
|
+
# Should syntax
|
518
|
+
it { should be_a_closure_tree.ordered }
|
519
|
+
# Expect syntax
|
520
|
+
it { is_expected.to be_a_closure_tree.ordered }
|
521
|
+
end
|
522
|
+
|
523
|
+
describe TodoList::Item do
|
524
|
+
# Should syntax
|
525
|
+
it { should be_a_closure_tree.ordered(:priority_order) }
|
526
|
+
# Expect syntax
|
527
|
+
it { is_expected.to be_a_closure_tree.ordered(:priority_order) }
|
528
|
+
end
|
529
|
+
|
530
|
+
```
|
531
|
+
|
532
|
+
|
487
533
|
## Testing
|
488
534
|
|
489
535
|
Closure tree is [tested under every valid combination](http://travis-ci.org/#!/mceachen/closure_tree) of
|
490
536
|
|
491
|
-
* Ruby 1.9.3
|
492
|
-
* Rubinius 2.2.
|
537
|
+
* Ruby 1.9.3, 2.1.2 (and sometimes head)
|
538
|
+
* Rubinius 2.2.1+ (and sometimes head)
|
539
|
+
* jRuby 1.9mode (and sometimes head)
|
493
540
|
* The latest Rails 3.2, 4.0, 4.1 and master branches
|
494
|
-
* Concurrency tests for MySQL and PostgreSQL. SQLite
|
541
|
+
* Concurrency tests for MySQL and PostgreSQL. SQLite is tested in a single-threaded environment.
|
495
542
|
|
496
543
|
Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
|
497
544
|
run the test matrix locally.
|
498
545
|
|
499
546
|
## Change log
|
500
547
|
|
501
|
-
See https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md
|
548
|
+
See the [change log](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md).
|
502
549
|
|
503
550
|
## Thanks to
|
504
551
|
|
552
|
+
* The more than 20 engineers around the world that have contributed their time and code to this gem
|
553
|
+
(see the [changelog](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md)!)
|
505
554
|
* https://github.com/collectiveidea/awesome_nested_set
|
506
555
|
* https://github.com/patshaughnessy/class_factory
|
507
556
|
* JetBrains, which provides an [open-source license](http://www.jetbrains.com/ruby/buy/buy.jsp#openSource) to
|
data/closure_tree.gemspec
CHANGED
@@ -14,22 +14,21 @@ Gem::Specification.new do |gem|
|
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
16
16
|
gem.test_files = gem.files.grep(%r{^spec/})
|
17
|
+
gem.required_ruby_version = '>= 1.9.3'
|
17
18
|
|
18
19
|
gem.add_runtime_dependency 'activerecord', '>= 3.2.0'
|
19
20
|
gem.add_runtime_dependency 'with_advisory_lock', '>= 0.0.9' # <- to prevent duplicate roots
|
20
21
|
|
21
22
|
gem.add_development_dependency 'rake'
|
22
23
|
gem.add_development_dependency 'yard'
|
23
|
-
gem.add_development_dependency 'rspec'
|
24
|
+
gem.add_development_dependency 'rspec', '~> 2.14.0' # FIXME: migrate to rspec 3 (or, better, ditch rspec and switch to minitest)
|
24
25
|
gem.add_development_dependency 'rspec-instafail'
|
25
26
|
gem.add_development_dependency 'rspec-rails' # FIXME: for rspec-rails and rspec fixture support
|
26
|
-
gem.add_development_dependency 'mysql2'
|
27
|
-
gem.add_development_dependency 'pg'
|
28
|
-
gem.add_development_dependency 'sqlite3'
|
29
27
|
gem.add_development_dependency 'uuidtools'
|
30
28
|
gem.add_development_dependency 'database_cleaner'
|
31
29
|
gem.add_development_dependency 'appraisal'
|
30
|
+
gem.add_development_dependency 'timecop'
|
32
31
|
|
33
32
|
# gem.add_development_dependency 'ruby-prof' # <- don't need this normally.
|
34
|
-
|
33
|
+
|
35
34
|
end
|
@@ -6,4 +6,16 @@ gem "foreigner", :git => "https://github.com/mceachen/foreigner.git"
|
|
6
6
|
gem "activerecord", "~> 3.2"
|
7
7
|
gem "strong_parameters"
|
8
8
|
|
9
|
+
platforms :ruby, :rbx do
|
10
|
+
gem "mysql2"
|
11
|
+
gem "pg"
|
12
|
+
gem "sqlite3"
|
13
|
+
end
|
14
|
+
|
15
|
+
platforms :jruby do
|
16
|
+
gem "activerecord-jdbcmysql-adapter"
|
17
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
18
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
19
|
+
end
|
20
|
+
|
9
21
|
gemspec :path => "../"
|
@@ -5,4 +5,16 @@ source "https://rubygems.org"
|
|
5
5
|
gem "foreigner", :git => "https://github.com/mceachen/foreigner.git"
|
6
6
|
gem "activerecord", "~> 4.0"
|
7
7
|
|
8
|
+
platforms :ruby, :rbx do
|
9
|
+
gem "mysql2"
|
10
|
+
gem "pg"
|
11
|
+
gem "sqlite3"
|
12
|
+
end
|
13
|
+
|
14
|
+
platforms :jruby do
|
15
|
+
gem "activerecord-jdbcmysql-adapter"
|
16
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
17
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
18
|
+
end
|
19
|
+
|
8
20
|
gemspec :path => "../"
|
@@ -5,4 +5,16 @@ source "https://rubygems.org"
|
|
5
5
|
gem "foreigner", :git => "https://github.com/mceachen/foreigner.git"
|
6
6
|
gem "activerecord", "~> 4.1"
|
7
7
|
|
8
|
+
platforms :ruby, :rbx do
|
9
|
+
gem "mysql2"
|
10
|
+
gem "pg"
|
11
|
+
gem "sqlite3"
|
12
|
+
end
|
13
|
+
|
14
|
+
platforms :jruby do
|
15
|
+
gem "activerecord-jdbcmysql-adapter"
|
16
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
17
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
18
|
+
end
|
19
|
+
|
8
20
|
gemspec :path => "../"
|
@@ -6,4 +6,16 @@ gem "foreigner", :git => "https://github.com/mceachen/foreigner.git"
|
|
6
6
|
gem "activerecord", :github => "rails/rails"
|
7
7
|
gem "arel", :github => "rails/arel"
|
8
8
|
|
9
|
+
platforms :ruby, :rbx do
|
10
|
+
gem "mysql2"
|
11
|
+
gem "pg"
|
12
|
+
gem "sqlite3"
|
13
|
+
end
|
14
|
+
|
15
|
+
platforms :jruby do
|
16
|
+
gem "activerecord-jdbcmysql-adapter"
|
17
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
18
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
19
|
+
end
|
20
|
+
|
9
21
|
gemspec :path => "../"
|
@@ -1,12 +1,4 @@
|
|
1
1
|
require 'with_advisory_lock'
|
2
|
-
require 'closure_tree/support'
|
3
|
-
require 'closure_tree/hierarchy_maintenance'
|
4
|
-
require 'closure_tree/model'
|
5
|
-
require 'closure_tree/finders'
|
6
|
-
require 'closure_tree/hash_tree'
|
7
|
-
require 'closure_tree/digraphs'
|
8
|
-
require 'closure_tree/deterministic_ordering'
|
9
|
-
require 'closure_tree/numeric_deterministic_ordering'
|
10
2
|
|
11
3
|
module ClosureTree
|
12
4
|
module ActsAsTree
|
@@ -20,7 +12,8 @@ module ClosureTree
|
|
20
12
|
:name_column,
|
21
13
|
:order,
|
22
14
|
:parent_column_name,
|
23
|
-
:with_advisory_lock
|
15
|
+
:with_advisory_lock,
|
16
|
+
:touch
|
24
17
|
)
|
25
18
|
|
26
19
|
class_attribute :_ct
|
@@ -4,6 +4,10 @@ module ClosureTree
|
|
4
4
|
read_attribute(_ct.order_column_sym)
|
5
5
|
end
|
6
6
|
|
7
|
+
def update_order_value(order_value)
|
8
|
+
update_column(_ct.order_column_sym, order_value)
|
9
|
+
end
|
10
|
+
|
7
11
|
def order_value=(new_order_value)
|
8
12
|
write_attribute(_ct.order_column_sym, new_order_value)
|
9
13
|
end
|
data/lib/closure_tree/finders.rb
CHANGED
@@ -34,7 +34,7 @@ module ClosureTree
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def find_all_by_generation(generation_level)
|
37
|
-
s = _ct.base_class.joins(<<-SQL)
|
37
|
+
s = _ct.base_class.joins(<<-SQL.strip_heredoc)
|
38
38
|
INNER JOIN (
|
39
39
|
SELECT descendant_id
|
40
40
|
FROM #{_ct.quoted_hierarchy_table_name}
|
@@ -75,7 +75,7 @@ module ClosureTree
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def leaves
|
78
|
-
s = joins(<<-SQL)
|
78
|
+
s = joins(<<-SQL.strip_heredoc)
|
79
79
|
INNER JOIN (
|
80
80
|
SELECT ancestor_id
|
81
81
|
FROM #{_ct.quoted_hierarchy_table_name}
|
@@ -96,7 +96,7 @@ module ClosureTree
|
|
96
96
|
end
|
97
97
|
|
98
98
|
def find_all_by_generation(generation_level)
|
99
|
-
s = joins(<<-SQL)
|
99
|
+
s = joins(<<-SQL.strip_heredoc)
|
100
100
|
INNER JOIN (
|
101
101
|
SELECT #{primary_key} as root_id
|
102
102
|
FROM #{_ct.quoted_table_name}
|
@@ -130,7 +130,7 @@ module ClosureTree
|
|
130
130
|
# MySQL doesn't support more than 61 joined tables (!!):
|
131
131
|
path.first(50).reverse.each_with_index do |ea, idx|
|
132
132
|
next_joined_table = "p#{idx}"
|
133
|
-
scope = scope.joins(<<-SQL)
|
133
|
+
scope = scope.joins(<<-SQL.strip_heredoc)
|
134
134
|
INNER JOIN #{_ct.quoted_table_name} AS #{next_joined_table}
|
135
135
|
ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
|
136
136
|
#{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
|
@@ -27,7 +27,7 @@ module ClosureTree
|
|
27
27
|
# Deepest generation, within limit, for each descendant
|
28
28
|
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
29
29
|
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
|
30
|
-
generation_depth = <<-SQL
|
30
|
+
generation_depth = <<-SQL.strip_heredoc
|
31
31
|
INNER JOIN (
|
32
32
|
SELECT descendant_id, MAX(generations) as depth
|
33
33
|
FROM #{_ct.quoted_hierarchy_table_name}
|
@@ -15,6 +15,10 @@ module ClosureTree
|
|
15
15
|
@_ct_skip_cycle_detection = true
|
16
16
|
end
|
17
17
|
|
18
|
+
def _ct_skip_sort_order_maintenance!
|
19
|
+
@_ct_skip_sort_order_maintenance = true
|
20
|
+
end
|
21
|
+
|
18
22
|
def _ct_validate
|
19
23
|
if !@_ct_skip_cycle_detection &&
|
20
24
|
!new_record? && # don't validate for cycles if we're a new record
|
@@ -32,7 +36,7 @@ module ClosureTree
|
|
32
36
|
|
33
37
|
def _ct_after_save
|
34
38
|
if changes[_ct.parent_column_name] || @was_new_record
|
35
|
-
rebuild!
|
39
|
+
rebuild! unless @_ct_skip_hierarchy_maintenance
|
36
40
|
end
|
37
41
|
if changes[_ct.parent_column_name] && !@was_new_record
|
38
42
|
# Resetting the ancestral collections addresses
|
@@ -41,6 +45,7 @@ module ClosureTree
|
|
41
45
|
self_and_ancestors.reload
|
42
46
|
end
|
43
47
|
@was_new_record = false # we aren't new anymore.
|
48
|
+
@_ct_skip_sort_order_maintenance = false # only skip once.
|
44
49
|
true # don't cancel anything.
|
45
50
|
end
|
46
51
|
|
@@ -54,12 +59,12 @@ module ClosureTree
|
|
54
59
|
true # don't prevent destruction
|
55
60
|
end
|
56
61
|
|
57
|
-
def rebuild!
|
62
|
+
def rebuild!(called_by_rebuild = false)
|
58
63
|
_ct.with_advisory_lock do
|
59
64
|
delete_hierarchy_references unless @was_new_record
|
60
65
|
hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
|
61
66
|
unless root?
|
62
|
-
_ct.connection.execute <<-SQL
|
67
|
+
_ct.connection.execute <<-SQL.strip_heredoc
|
63
68
|
INSERT INTO #{_ct.quoted_hierarchy_table_name}
|
64
69
|
(ancestor_id, descendant_id, generations)
|
65
70
|
SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1
|
@@ -67,8 +72,16 @@ module ClosureTree
|
|
67
72
|
WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)}
|
68
73
|
SQL
|
69
74
|
end
|
70
|
-
|
71
|
-
|
75
|
+
|
76
|
+
if _ct.order_is_numeric? && !@_ct_skip_sort_order_maintenance
|
77
|
+
_ct_reorder_prior_siblings_if_parent_changed
|
78
|
+
# Prevent double-reordering of siblings:
|
79
|
+
_ct_reorder_siblings if !called_by_rebuild
|
80
|
+
end
|
81
|
+
|
82
|
+
children.each { |c| c.rebuild!(true) }
|
83
|
+
|
84
|
+
_ct_reorder_children if _ct.order_is_numeric? && children.present?
|
72
85
|
end
|
73
86
|
end
|
74
87
|
|
@@ -78,7 +91,7 @@ module ClosureTree
|
|
78
91
|
# It shouldn't affect performance of postgresql.
|
79
92
|
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
80
93
|
# Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
|
81
|
-
_ct.connection.execute <<-SQL
|
94
|
+
_ct.connection.execute <<-SQL.strip_heredoc
|
82
95
|
DELETE FROM #{_ct.quoted_hierarchy_table_name}
|
83
96
|
WHERE descendant_id IN (
|
84
97
|
SELECT DISTINCT descendant_id
|
data/lib/closure_tree/model.rb
CHANGED
@@ -8,7 +8,8 @@ module ClosureTree
|
|
8
8
|
belongs_to :parent,
|
9
9
|
class_name: _ct.model_class.to_s,
|
10
10
|
foreign_key: _ct.parent_column_name,
|
11
|
-
inverse_of: :children
|
11
|
+
inverse_of: :children,
|
12
|
+
touch: _ct.options[:touch]
|
12
13
|
|
13
14
|
# TODO, remove when activerecord 3.2 support is dropped
|
14
15
|
attr_accessible :parent if _ct.use_attr_accessible?
|
@@ -148,14 +149,5 @@ module ClosureTree
|
|
148
149
|
def _ct_quoted_id
|
149
150
|
_ct.quoted_value(_ct_id)
|
150
151
|
end
|
151
|
-
|
152
|
-
def _ct_update_column(column, value)
|
153
|
-
if respond_to?(:update_column)
|
154
|
-
update_column(column, value)
|
155
|
-
else
|
156
|
-
# This will run callbacks, but it's better than failing outright:
|
157
|
-
update_attribute(column, value)
|
158
|
-
end
|
159
|
-
end
|
160
152
|
end
|
161
153
|
end
|
@@ -6,19 +6,23 @@ module ClosureTree
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
after_destroy :
|
9
|
+
after_destroy :_ct_reorder_siblings
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
|
12
|
+
def _ct_reorder_prior_siblings_if_parent_changed
|
13
|
+
if attribute_changed?(_ct.parent_column_name) && !@was_new_record
|
14
|
+
was_parent_id = attribute_was(_ct.parent_column_name)
|
15
|
+
_ct.reorder_with_parent_id(was_parent_id)
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
|
-
def _ct_reorder_siblings(minimum_sort_order_value = nil
|
17
|
-
_ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value
|
19
|
+
def _ct_reorder_siblings(minimum_sort_order_value = nil)
|
20
|
+
_ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value)
|
21
|
+
reload unless destroyed?
|
18
22
|
end
|
19
23
|
|
20
|
-
def _ct_reorder_children(minimum_sort_order_value = nil
|
21
|
-
_ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value
|
24
|
+
def _ct_reorder_children(minimum_sort_order_value = nil)
|
25
|
+
_ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value)
|
22
26
|
end
|
23
27
|
|
24
28
|
def self_and_descendants_preordered
|
@@ -46,13 +50,13 @@ module ClosureTree
|
|
46
50
|
|
47
51
|
module ClassMethods
|
48
52
|
def roots_and_descendants_preordered
|
49
|
-
h = _ct.connection.select_one(<<-SQL)
|
53
|
+
h = _ct.connection.select_one(<<-SQL.strip_heredoc)
|
50
54
|
SELECT
|
51
55
|
count(*) as total_descendants,
|
52
56
|
max(generations) as max_depth
|
53
57
|
FROM #{_ct.quoted_hierarchy_table_name}
|
54
58
|
SQL
|
55
|
-
join_sql = <<-SQL
|
59
|
+
join_sql = <<-SQL.strip_heredoc
|
56
60
|
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
57
61
|
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.id
|
58
62
|
JOIN #{_ct.quoted_table_name} anc
|
@@ -70,6 +74,19 @@ module ClosureTree
|
|
70
74
|
end
|
71
75
|
end
|
72
76
|
|
77
|
+
def append_child(child_node)
|
78
|
+
add_child(child_node)
|
79
|
+
end
|
80
|
+
|
81
|
+
def prepend_child(child_node)
|
82
|
+
child_node.order_value = -1
|
83
|
+
child_node.parent = self
|
84
|
+
child_node._ct_skip_sort_order_maintenance!
|
85
|
+
child_node.save
|
86
|
+
_ct_reorder_children
|
87
|
+
child_node.reload
|
88
|
+
end
|
89
|
+
|
73
90
|
def append_sibling(sibling_node)
|
74
91
|
add_sibling(sibling_node, true)
|
75
92
|
end
|
@@ -80,39 +97,39 @@ module ClosureTree
|
|
80
97
|
|
81
98
|
def add_sibling(sibling, add_after = true)
|
82
99
|
fail "can't add self as sibling" if self == sibling
|
100
|
+
|
101
|
+
# Make sure self isn't dirty, because we're going to call reload:
|
102
|
+
save
|
103
|
+
|
83
104
|
_ct.with_advisory_lock do
|
84
|
-
if self.order_value.nil?
|
85
|
-
# ergh, we don't know where we stand within the siblings, so establish that first:
|
86
|
-
_ct_reorder_siblings
|
87
|
-
reload # < because self.order_value changed
|
88
|
-
end
|
89
105
|
prior_sibling_parent = sibling.parent
|
90
|
-
if prior_sibling_parent == self.parent
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
_ct_reorder_siblings(sibling.order_value, 0)
|
95
|
-
reload # < because self.order_value changed
|
96
|
-
end
|
97
|
-
end
|
98
|
-
_ct_move_new_sibling(sibling, add_after)
|
99
|
-
if prior_sibling_parent && prior_sibling_parent != self.parent
|
100
|
-
prior_sibling_parent._ct_reorder_children
|
106
|
+
reorder_from_value = if prior_sibling_parent == self.parent
|
107
|
+
[self.order_value, sibling.order_value].compact.min
|
108
|
+
else
|
109
|
+
self.order_value
|
101
110
|
end
|
102
|
-
sibling
|
103
|
-
end
|
104
|
-
end
|
105
111
|
|
106
|
-
def _ct_move_new_sibling(sibling, add_after)
|
107
|
-
_ct_reorder_siblings(self.order_value + 1, 1)
|
108
|
-
if add_after
|
109
|
-
sibling.order_value = self.order_value + 1
|
110
|
-
else
|
111
112
|
sibling.order_value = self.order_value
|
112
|
-
|
113
|
-
|
113
|
+
sibling.parent = self.parent
|
114
|
+
sibling._ct_skip_sort_order_maintenance!
|
115
|
+
sibling.save # may be a no-op
|
116
|
+
|
117
|
+
_ct_reorder_siblings(reorder_from_value)
|
118
|
+
|
119
|
+
# The sort order should be correct now except for self and sibling, which may need to flip:
|
120
|
+
sibling_is_after = self.reload.sort_order < sibling.reload.sort_order
|
121
|
+
if add_after != sibling_is_after
|
122
|
+
# We need to flip the sort orders:
|
123
|
+
self_so, sib_so = self.sort_order, sibling.sort_order
|
124
|
+
update_order_value(sib_so)
|
125
|
+
sibling.update_order_value(self_so)
|
126
|
+
end
|
127
|
+
|
128
|
+
if prior_sibling_parent != self.parent
|
129
|
+
prior_sibling_parent.try(:_ct_reorder_children)
|
130
|
+
end
|
131
|
+
sibling
|
114
132
|
end
|
115
|
-
parent.add_child(sibling) # <- this causes sibling to be saved.
|
116
133
|
end
|
117
134
|
end
|
118
135
|
end
|