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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +11 -3
  4. data/CHANGELOG.md +13 -0
  5. data/Gemfile +13 -0
  6. data/README.md +61 -12
  7. data/closure_tree.gemspec +4 -5
  8. data/gemfiles/activerecord_3.2.gemfile +12 -0
  9. data/gemfiles/activerecord_4.0.gemfile +12 -0
  10. data/gemfiles/activerecord_4.1.gemfile +12 -0
  11. data/gemfiles/activerecord_edge.gemfile +12 -0
  12. data/lib/closure_tree/acts_as_tree.rb +2 -9
  13. data/lib/closure_tree/deterministic_ordering.rb +4 -0
  14. data/lib/closure_tree/finders.rb +4 -4
  15. data/lib/closure_tree/hash_tree.rb +1 -1
  16. data/lib/closure_tree/hierarchy_maintenance.rb +19 -6
  17. data/lib/closure_tree/model.rb +2 -10
  18. data/lib/closure_tree/numeric_deterministic_ordering.rb +53 -36
  19. data/lib/closure_tree/numeric_order_support.rb +20 -13
  20. data/lib/closure_tree/support.rb +8 -0
  21. data/lib/closure_tree/support_attributes.rb +10 -0
  22. data/lib/closure_tree/test/matcher.rb +85 -0
  23. data/lib/closure_tree/version.rb +1 -1
  24. data/lib/closure_tree.rb +14 -1
  25. data/spec/cache_invalidation_spec.rb +39 -0
  26. data/spec/{support → db}/models.rb +6 -0
  27. data/spec/db/schema.rb +18 -0
  28. data/spec/label_spec.rb +171 -46
  29. data/spec/matcher_spec.rb +32 -0
  30. data/spec/parallel_spec.rb +85 -49
  31. data/spec/spec_helper.rb +6 -96
  32. data/spec/support/database.rb +49 -0
  33. data/spec/support/database_cleaner.rb +14 -0
  34. data/spec/support/deprecated/attr_accessible.rb +5 -0
  35. data/spec/support/hash_monkey_patch.rb +13 -0
  36. data/spec/support/helpers.rb +8 -0
  37. data/spec/support/sqlite3_with_advisory_lock.rb +10 -0
  38. data/tests.sh +7 -2
  39. metadata +31 -43
  40. data/spec/parallel_prepend_sibling_spec.rb +0 -42
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ceda99255848a84a4d3370654ecfad42dc2b7711
4
- data.tar.gz: 71096e3755bb3dda8039eb49e3f301930ef7cc0d
3
+ metadata.gz: 74e74ddf4f763041d38464015bd608db38833721
4
+ data.tar.gz: 91aeb3af057e67f9b97576ef7cdd2bce155572dd
5
5
  SHA512:
6
- metadata.gz: f40621b5ddc3cbc1c0de1959f2720b7f1e21e209ed7952878720cbcff981993037330a9c6f1e6f4f287e862b2af82e14b99109dde5a2330c6cb5e3256a0f7acb
7
- data.tar.gz: ecd521d391c33e3afb162c25f8760c734dd58acec6c3a875d523ec7ec5dcfb50ccbbd7601f79d2fd68e2a1e592095174381508a8e45708dd91bcf1bd658cf0a7
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
- - ruby-head
6
- # - rbx-2
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(:name => "root")
392
- a = OrderedTag.create(:name => "a", :parent => root)
393
- b = OrderedTag.create(:name => "b")
394
- c = OrderedTag.create(:name => "c")
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.collect(&:name)
409
+ root.reload.children.pluck(:name)
400
410
  => ["a", "b"]
401
411
 
402
412
  a.prepend_sibling(b)
403
- root.reload.children.collect(&:name)
413
+ root.reload.children.pluck(:name)
404
414
  => ["b", "a"]
405
415
 
406
416
  a.append_sibling(c)
407
- root.reload.children.collect(&:name)
417
+ root.reload.children.pluck(:name)
408
418
  => ["b", "a", "c"]
409
419
 
410
420
  b.append_sibling(c)
411
- root.reload.children.collect(&:name)
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 , 2.0.0 and 2.1.2
492
- * Rubinius 2.2.6
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 works in a single-threaded environment.
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
- # TODO: gem 'activerecord-jdbcsqlite3-adapter', :platform => :jruby
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
@@ -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
- children.each { |c| c.rebuild! }
71
- _ct_reorder_children if _ct.order_is_numeric?
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
@@ -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 :_ct_reorder_after_destroy
9
+ after_destroy :_ct_reorder_siblings
10
10
  end
11
11
 
12
- def _ct_reorder_after_destroy
13
- _ct_reorder_siblings
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, delta = 0)
17
- _ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value, delta)
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, delta = 0)
21
- _ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value, delta)
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
- # We have to adjust the prior siblings by moving sibling out of the way:
92
- sibling._ct_update_column(_ct.parent_column_sym, nil)
93
- if sibling.order_value && sibling.order_value < self.order_value
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
- self.order_value += 1
113
- self.save!
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