closure_tree 4.5.0 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
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