acts_as_list 0.7.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +5 -13
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +123 -0
  5. data/.gitignore +1 -0
  6. data/.travis.yml +50 -12
  7. data/Appraisals +39 -6
  8. data/CHANGELOG.md +565 -148
  9. data/Gemfile +19 -14
  10. data/README.md +206 -19
  11. data/Rakefile +4 -4
  12. data/acts_as_list.gemspec +16 -11
  13. data/gemfiles/rails_4_2.gemfile +18 -9
  14. data/gemfiles/rails_5_0.gemfile +31 -0
  15. data/gemfiles/rails_5_1.gemfile +31 -0
  16. data/gemfiles/rails_5_2.gemfile +31 -0
  17. data/gemfiles/rails_6_0.gemfile +31 -0
  18. data/gemfiles/rails_6_1.gemfile +31 -0
  19. data/gemfiles/rails_7_0.gemfile +31 -0
  20. data/init.rb +2 -0
  21. data/lib/acts_as_list/active_record/acts/active_record.rb +5 -0
  22. data/lib/acts_as_list/active_record/acts/add_new_at_method_definer.rb +11 -0
  23. data/lib/acts_as_list/active_record/acts/aux_method_definer.rb +11 -0
  24. data/lib/acts_as_list/active_record/acts/callback_definer.rb +19 -0
  25. data/lib/acts_as_list/active_record/acts/list.rb +299 -306
  26. data/lib/acts_as_list/active_record/acts/no_update.rb +125 -0
  27. data/lib/acts_as_list/active_record/acts/position_column_method_definer.rb +101 -0
  28. data/lib/acts_as_list/active_record/acts/scope_method_definer.rb +77 -0
  29. data/lib/acts_as_list/active_record/acts/sequential_updates_method_definer.rb +28 -0
  30. data/lib/acts_as_list/active_record/acts/top_of_list_method_definer.rb +15 -0
  31. data/lib/acts_as_list/version.rb +3 -1
  32. data/lib/acts_as_list.rb +11 -14
  33. data/test/database.yml +18 -0
  34. data/test/helper.rb +50 -2
  35. data/test/shared.rb +3 -0
  36. data/test/shared_array_scope_list.rb +21 -4
  37. data/test/shared_list.rb +86 -12
  38. data/test/shared_list_sub.rb +63 -2
  39. data/test/shared_no_addition.rb +50 -2
  40. data/test/shared_quoting.rb +23 -0
  41. data/test/shared_top_addition.rb +36 -13
  42. data/test/shared_zero_based.rb +13 -0
  43. data/test/test_default_scope_with_select.rb +33 -0
  44. data/test/test_joined_list.rb +61 -0
  45. data/test/test_list.rb +601 -84
  46. data/test/test_no_update_for_extra_classes.rb +131 -0
  47. data/test/test_no_update_for_scope_destruction.rb +69 -0
  48. data/test/test_no_update_for_subclasses.rb +56 -0
  49. data/test/test_scope_with_user_defined_foreign_key.rb +42 -0
  50. metadata +56 -22
  51. data/gemfiles/rails_3_2.gemfile +0 -24
  52. data/gemfiles/rails_4_1.gemfile +0 -24
data/test/test_list.rb CHANGED
@@ -1,13 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # NOTE: following now done in helper.rb (better Readability)
2
4
  require 'helper'
3
5
 
4
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
5
- ActiveRecord::Schema.verbose = false
6
-
7
6
  def setup_db(position_options = {})
7
+ $default_position = position_options[:default]
8
+
9
+ # sqlite cannot drop/rename/alter columns and add constraints after table creation
10
+ sqlite = ENV.fetch("DB", "sqlite") == "sqlite"
11
+
8
12
  # AR caches columns options like defaults etc. Clear them!
9
13
  ActiveRecord::Base.connection.create_table :mixins do |t|
10
- t.column :pos, :integer, position_options
14
+ t.column :pos, :integer, **position_options unless position_options[:positive] && sqlite
11
15
  t.column :active, :boolean, default: true
12
16
  t.column :parent_id, :integer
13
17
  t.column :parent_type, :string
@@ -16,11 +20,37 @@ def setup_db(position_options = {})
16
20
  t.column :state, :integer
17
21
  end
18
22
 
19
- mixins = [ Mixin, ListMixin, ListMixinSub1, ListMixinSub2, ListWithStringScopeMixin,
20
- ArrayScopeListMixin, ZeroBasedMixin, DefaultScopedMixin,
21
- DefaultScopedWhereMixin, TopAdditionMixin, NoAdditionMixin ]
23
+ if position_options[:unique] && !(sqlite && position_options[:positive])
24
+ ActiveRecord::Base.connection.add_index :mixins, :pos, unique: true
25
+ end
26
+
27
+ if position_options[:positive]
28
+ if sqlite
29
+ # SQLite cannot add constraint after table creation, also cannot add unique inside ADD COLUMN
30
+ ActiveRecord::Base.connection.execute('ALTER TABLE mixins ADD COLUMN pos integer8 NOT NULL CHECK (pos > 0) DEFAULT 1')
31
+ ActiveRecord::Base.connection.execute('CREATE UNIQUE INDEX index_mixins_on_pos ON mixins(pos)')
32
+ else
33
+ ActiveRecord::Base.connection.execute('ALTER TABLE mixins ADD CONSTRAINT pos_check CHECK (pos > 0)')
34
+ end
35
+ end
22
36
 
23
- mixins << EnumArrayScopeListMixin if rails_4
37
+ # This table is used to test table names and column names quoting
38
+ ActiveRecord::Base.connection.create_table 'table-name' do |t|
39
+ t.column :order, :integer
40
+ end
41
+
42
+ # This table is used to test table names with different primary_key columns
43
+ ActiveRecord::Base.connection.create_table 'altid-table', primary_key: 'altid' do |t|
44
+ t.column :pos, :integer
45
+ t.column :created_at, :datetime
46
+ t.column :updated_at, :datetime
47
+ end
48
+
49
+ ActiveRecord::Base.connection.add_index 'altid-table', :pos, unique: true
50
+
51
+ mixins = [ Mixin, ListMixin, ListMixinSub1, ListMixinSub2, ListWithStringScopeMixin,
52
+ ArrayScopeListMixin, ZeroBasedMixin, DefaultScopedMixin, EnumArrayScopeListMixin,
53
+ DefaultScopedWhereMixin, TopAdditionMixin, NoAdditionMixin, QuotedList, TouchDisabledMixin ]
24
54
 
25
55
  ActiveRecord::Base.connection.schema_cache.clear!
26
56
  mixins.each do |klass|
@@ -32,21 +62,6 @@ def setup_db_with_default
32
62
  setup_db default: 0
33
63
  end
34
64
 
35
- # Returns true if ActiveRecord is rails3,4 version
36
- def rails_3
37
- defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
38
- end
39
-
40
- def rails_4
41
- defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 4
42
- end
43
-
44
- def teardown_db
45
- ActiveRecord::Base.connection.tables.each do |table|
46
- ActiveRecord::Base.connection.drop_table(table)
47
- end
48
- end
49
-
50
65
  class Mixin < ActiveRecord::Base
51
66
  self.table_name = 'mixins'
52
67
  end
@@ -55,15 +70,19 @@ class ListMixin < Mixin
55
70
  acts_as_list column: "pos", scope: :parent
56
71
  end
57
72
 
73
+ class TouchDisabledMixin < Mixin
74
+ acts_as_list column: "pos", touch_on_update: false
75
+ end
76
+
58
77
  class ListMixinSub1 < ListMixin
59
78
  end
60
79
 
61
80
  class ListMixinSub2 < ListMixin
62
- if rails_3
63
- validates :pos, presence: true
64
- else
65
- validates_presence_of :pos
66
- end
81
+ validates :pos, presence: true
82
+ end
83
+
84
+ class ListMixinError < ListMixin
85
+ validates :state, presence: true
67
86
  end
68
87
 
69
88
  class ListWithStringScopeMixin < Mixin
@@ -74,13 +93,15 @@ class ArrayScopeListMixin < Mixin
74
93
  acts_as_list column: "pos", scope: [:parent_id, :parent_type]
75
94
  end
76
95
 
77
- if rails_4
78
- class EnumArrayScopeListMixin < Mixin
79
- STATE_VALUES = %w(active archived)
80
- enum state: STATE_VALUES
96
+ class ArrayScopeListWithHashMixin < Mixin
97
+ acts_as_list column: "pos", scope: [:parent_id, state: nil]
98
+ end
81
99
 
82
- acts_as_list column: "pos", scope: [:parent_id, :state]
83
- end
100
+ class EnumArrayScopeListMixin < Mixin
101
+ STATE_VALUES = %w(active archived)
102
+ enum state: STATE_VALUES
103
+
104
+ acts_as_list column: "pos", scope: [:parent_id, :state]
84
105
  end
85
106
 
86
107
  class ZeroBasedMixin < Mixin
@@ -97,10 +118,29 @@ class DefaultScopedWhereMixin < Mixin
97
118
  default_scope { order('pos ASC').where(active: true) }
98
119
 
99
120
  def self.for_active_false_tests
100
- unscoped.order('pos ASC').where(active: false)
121
+ unscope(:where).where(active: false)
101
122
  end
102
123
  end
103
124
 
125
+ class SequentialUpdatesDefault < Mixin
126
+ acts_as_list column: "pos"
127
+ end
128
+
129
+ class SequentialUpdatesAltId < ActiveRecord::Base
130
+ self.table_name = "altid-table"
131
+ self.primary_key = "altid"
132
+
133
+ acts_as_list column: "pos"
134
+ end
135
+
136
+ class SequentialUpdatesAltIdTouchDisabled < SequentialUpdatesAltId
137
+ acts_as_list column: "pos", touch_on_update: false
138
+ end
139
+
140
+ class SequentialUpdatesFalseMixin < Mixin
141
+ acts_as_list column: "pos", sequential_updates: false
142
+ end
143
+
104
144
  class TopAdditionMixin < Mixin
105
145
  acts_as_list column: "pos", add_new_at: :top, scope: :parent_id
106
146
  end
@@ -112,7 +152,7 @@ end
112
152
  ##
113
153
  # The way we track changes to
114
154
  # scope and position can get tripped up
115
- # by someone using update_attributes within
155
+ # by someone using update within
116
156
  # a callback because it causes multiple passes
117
157
  # through the callback chain
118
158
  module CallbackMixin
@@ -127,7 +167,7 @@ module CallbackMixin
127
167
  # doesn't matter what column changes, just
128
168
  # need to change something
129
169
 
130
- self.update_attributes(active: !self.active)
170
+ self.update active: !self.active
131
171
  end
132
172
  end
133
173
  end
@@ -149,6 +189,11 @@ end
149
189
  class TheBaseSubclass < TheBaseClass
150
190
  end
151
191
 
192
+ class QuotedList < ActiveRecord::Base
193
+ self.table_name = 'table-name'
194
+ acts_as_list column: :order
195
+ end
196
+
152
197
  class ActsAsListTestCase < Minitest::Test
153
198
  # No default test required as this class is abstract.
154
199
  # Need for test/unit.
@@ -184,6 +229,37 @@ class ListTest < ActsAsListTestCase
184
229
  setup_db
185
230
  super
186
231
  end
232
+
233
+ def test_insert_race_condition
234
+ # the bigger n is the more likely we will have a race condition
235
+ n = 1000
236
+ (1..n).each do |counter|
237
+ node = ListMixin.new parent_id: 1
238
+ node.pos = counter
239
+ node.save!
240
+ end
241
+
242
+ wait_for_it = true
243
+ threads = []
244
+ 4.times do |i|
245
+ threads << Thread.new do
246
+ true while wait_for_it
247
+ ActiveRecord::Base.connection_pool.with_connection do |c|
248
+ n.times do
249
+ begin
250
+ ListMixin.where(parent_id: 1).order('pos').last.insert_at(1)
251
+ rescue Exception
252
+ # ignore SQLite3::SQLException due to table locking
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ wait_for_it = false
259
+ threads.each(&:join)
260
+
261
+ assert_equal((1..n).to_a, ListMixin.where(parent_id: 1).order('pos').map(&:pos))
262
+ end
187
263
  end
188
264
 
189
265
  class ListWithCallbackTest < ActsAsListTestCase
@@ -243,6 +319,15 @@ class ArrayScopeListTestWithDefault < ActsAsListTestCase
243
319
  end
244
320
  end
245
321
 
322
+ class QuotingTestList < ActsAsListTestCase
323
+ include Shared::Quoting
324
+
325
+ def setup
326
+ setup_db_with_default
327
+ super
328
+ end
329
+ end
330
+
246
331
  class DefaultScopedTest < ActsAsListTestCase
247
332
  def setup
248
333
  setup_db
@@ -260,6 +345,11 @@ class DefaultScopedTest < ActsAsListTestCase
260
345
  assert !new.first?
261
346
  assert new.last?
262
347
 
348
+ new = DefaultScopedMixin.acts_as_list_no_update { DefaultScopedMixin.create }
349
+ assert_equal_or_nil $default_position, new.pos
350
+ assert_equal $default_position.is_a?(Integer), new.first?
351
+ assert !new.last?
352
+
263
353
  new = DefaultScopedMixin.create
264
354
  assert_equal 7, new.pos
265
355
  assert !new.first?
@@ -295,6 +385,9 @@ class DefaultScopedTest < ActsAsListTestCase
295
385
  new = DefaultScopedMixin.create
296
386
  assert_equal 6, new.pos
297
387
 
388
+ new_noup = DefaultScopedMixin.acts_as_list_no_update { DefaultScopedMixin.create }
389
+ assert_equal_or_nil $default_position, new_noup.pos
390
+
298
391
  new = DefaultScopedMixin.create
299
392
  assert_equal 7, new.pos
300
393
 
@@ -321,6 +414,15 @@ class DefaultScopedTest < ActsAsListTestCase
321
414
 
322
415
  new4.reload
323
416
  assert_equal 4, new4.pos
417
+
418
+ new_noup.reload
419
+ assert_equal_or_nil $default_position, new_noup.pos
420
+ end
421
+
422
+ def test_find_or_create_doesnt_raise_deprecation_warning
423
+ assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
424
+ DefaultScopedMixin.find_or_create_by(pos: 5)
425
+ end
324
426
  end
325
427
 
326
428
  def test_update_position
@@ -353,6 +455,11 @@ class DefaultScopedWhereTest < ActsAsListTestCase
353
455
  assert !new.first?
354
456
  assert new.last?
355
457
 
458
+ new = DefaultScopedWhereMixin.acts_as_list_no_update { DefaultScopedWhereMixin.create }
459
+ assert_equal_or_nil $default_position, new.pos
460
+ assert_equal $default_position.is_a?(Integer), new.first?
461
+ assert !new.last?
462
+
356
463
  new = DefaultScopedWhereMixin.create
357
464
  assert_equal 7, new.pos
358
465
  assert !new.first?
@@ -391,6 +498,9 @@ class DefaultScopedWhereTest < ActsAsListTestCase
391
498
  new = DefaultScopedWhereMixin.create
392
499
  assert_equal 7, new.pos
393
500
 
501
+ new_noup = DefaultScopedWhereMixin.acts_as_list_no_update { DefaultScopedWhereMixin.create }
502
+ assert_equal_or_nil $default_position, new_noup.pos
503
+
394
504
  new4 = DefaultScopedWhereMixin.create
395
505
  assert_equal 8, new4.pos
396
506
 
@@ -414,6 +524,15 @@ class DefaultScopedWhereTest < ActsAsListTestCase
414
524
 
415
525
  new4.reload
416
526
  assert_equal 4, new4.pos
527
+
528
+ new_noup.reload
529
+ assert_equal_or_nil $default_position, new_noup.pos
530
+ end
531
+
532
+ def test_find_or_create_doesnt_raise_deprecation_warning
533
+ assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
534
+ DefaultScopedWhereMixin.find_or_create_by(pos: 5)
535
+ end
417
536
  end
418
537
 
419
538
  def test_update_position
@@ -468,10 +587,56 @@ class MultiDestroyTest < ActsAsListTestCase
468
587
  new3 = DefaultScopedMixin.create
469
588
  assert_equal 3, new3.pos
470
589
 
590
+ new4 = DefaultScopedMixin.create
591
+ assert_equal 4, new4.pos
592
+
471
593
  new1.destroy
472
594
  new2.destroy
473
595
  new3.reload
596
+ new4.reload
474
597
  assert_equal 1, new3.pos
598
+ assert_equal 2, new4.pos
599
+
600
+ DefaultScopedMixin.acts_as_list_no_update { new3.destroy }
601
+
602
+ new4.reload
603
+ assert_equal 2, new4.pos
604
+ end
605
+ end
606
+
607
+ class MultiUpdateTest < ActsAsListTestCase
608
+
609
+ def setup
610
+ setup_db
611
+ end
612
+
613
+ def test_multiple_updates_within_transaction
614
+ @page = ListMixin.create! id: 100, parent_id: nil, pos: 1
615
+ @row = ListMixin.create! parent_id: @page.id, pos: 1
616
+ @column1 = ListMixin.create! parent_id: @row.id, pos: 1
617
+ @column2 = ListMixin.create! parent_id: @row.id, pos: 2
618
+ @rich_text1 = ListMixin.create! parent_id: @column1.id, pos: 1
619
+ @rich_text2 = ListMixin.create! parent_id: @column2.id, pos: 1
620
+
621
+ ActiveRecord::Base.transaction do
622
+ @rich_text1.update!(parent_id: @column2.id, pos: 1)
623
+
624
+ assert_equal [@rich_text1.id, @rich_text2.id], ListMixin.where(parent_id: @column2.id).order('pos').map(&:id)
625
+ assert_equal [1, 2], ListMixin.where(parent_id: @column2.id).order('pos').map(&:pos)
626
+
627
+ @column1.destroy!
628
+ assert_equal [@column2.id], ListMixin.where(parent_id: @row.id).order('pos').map(&:id)
629
+ assert_equal [1], ListMixin.where(parent_id: @row.id).order('pos').map(&:pos)
630
+
631
+ @rich_text1.update!(parent_id: @page.id, pos: 1)
632
+ @rich_text2.update!(parent_id: @page.id, pos: 2)
633
+ @row.destroy!
634
+ @column2.destroy!
635
+ end
636
+
637
+ assert_equal(1, @page.reload.pos)
638
+ assert_equal [@rich_text1.id, @rich_text2.id], ListMixin.where(parent_id: @page.id).order('pos').map(&:id)
639
+ assert_equal [1, 2], ListMixin.where(parent_id: @page.id).order('pos').map(&:pos)
475
640
  end
476
641
  end
477
642
 
@@ -512,38 +677,48 @@ class MultipleListsTest < ActsAsListTestCase
512
677
  end
513
678
 
514
679
  def test_check_scope_order
515
- assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 1).order(:pos).map(&:id)
516
- assert_equal [5, 6, 7, 8], ListMixin.where(:parent_id => 2).order(:pos).map(&:id)
517
- ListMixin.find(4).update_attributes(:parent_id => 2, :pos => 2)
518
- assert_equal [1, 2, 3], ListMixin.where(:parent_id => 1).order(:pos).map(&:id)
519
- assert_equal [5, 4, 6, 7, 8], ListMixin.where(:parent_id => 2).order(:pos).map(&:id)
680
+ assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 1).order('pos').map(&:id)
681
+ assert_equal [5, 6, 7, 8], ListMixin.where(:parent_id => 2).order('pos').map(&:id)
682
+ ListMixin.find(4).update :parent_id => 2, :pos => 2
683
+ assert_equal [1, 2, 3], ListMixin.where(:parent_id => 1).order('pos').map(&:id)
684
+ assert_equal [5, 4, 6, 7, 8], ListMixin.where(:parent_id => 2).order('pos').map(&:id)
520
685
  end
521
686
 
522
687
  def test_check_scope_position
523
688
  assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 1).map(&:pos)
524
689
  assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 2).map(&:pos)
525
- ListMixin.find(4).update_attributes(:parent_id => 2, :pos => 2)
526
- assert_equal [1, 2, 3], ListMixin.where(:parent_id => 1).order(:pos).map(&:pos)
527
- assert_equal [1, 2, 3, 4, 5], ListMixin.where(:parent_id => 2).order(:pos).map(&:pos)
690
+ ListMixin.find(4).update :parent_id => 2, :pos => 2
691
+ assert_equal [1, 2, 3], ListMixin.where(:parent_id => 1).order('pos').map(&:pos)
692
+ assert_equal [1, 2, 3, 4, 5], ListMixin.where(:parent_id => 2).order('pos').map(&:pos)
528
693
  end
529
- end
530
694
 
531
- if rails_4
532
- class EnumArrayScopeListMixinTest < ActsAsListTestCase
533
- def setup
534
- setup_db
535
- EnumArrayScopeListMixin.create! :parent_id => 1, :state => EnumArrayScopeListMixin.states['active']
536
- EnumArrayScopeListMixin.create! :parent_id => 1, :state => EnumArrayScopeListMixin.states['archived']
537
- EnumArrayScopeListMixin.create! :parent_id => 2, :state => EnumArrayScopeListMixin.states["active"]
538
- EnumArrayScopeListMixin.create! :parent_id => 2, :state => EnumArrayScopeListMixin.states["archived"]
695
+ def test_find_or_create_doesnt_raise_deprecation_warning
696
+ assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
697
+ ListMixin.where(:parent_id => 1).find_or_create_by(pos: 5)
539
698
  end
699
+ end
700
+ end
540
701
 
541
- def test_positions
542
- assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 1, :state => EnumArrayScopeListMixin.states['active']).map(&:pos)
543
- assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 1, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos)
544
- assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['active']).map(&:pos)
545
- assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos)
546
- end
702
+ class EnumArrayScopeListMixinTest < ActsAsListTestCase
703
+ def setup
704
+ setup_db
705
+ EnumArrayScopeListMixin.create! :parent_id => 1, :state => EnumArrayScopeListMixin.states['active']
706
+ EnumArrayScopeListMixin.create! :parent_id => 1, :state => EnumArrayScopeListMixin.states['archived']
707
+ EnumArrayScopeListMixin.create! :parent_id => 2, :state => EnumArrayScopeListMixin.states["active"]
708
+ EnumArrayScopeListMixin.create! :parent_id => 2, :state => EnumArrayScopeListMixin.states["archived"]
709
+ end
710
+
711
+ def test_positions
712
+ assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 1, :state => EnumArrayScopeListMixin.states['active']).map(&:pos)
713
+ assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 1, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos)
714
+ assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['active']).map(&:pos)
715
+ assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos)
716
+ end
717
+
718
+ def test_update_state
719
+ active_item = EnumArrayScopeListMixin.find_by(:parent_id => 2, :state => EnumArrayScopeListMixin.states['active'])
720
+ active_item.update(state: EnumArrayScopeListMixin.states['archived'])
721
+ assert_equal [1, 2], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos).sort
547
722
  end
548
723
  end
549
724
 
@@ -556,50 +731,392 @@ class MultipleListsArrayScopeTest < ActsAsListTestCase
556
731
  end
557
732
 
558
733
  def test_order_after_all_scope_properties_are_changed
559
- assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order(:pos).map(&:id)
560
- assert_equal [5, 6, 7, 8], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').order(:pos).map(&:id)
561
- ArrayScopeListMixin.find(2).update_attributes(:parent_id => 2, :pos => 2,:parent_type => 'something')
562
- assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order(:pos).map(&:id)
563
- assert_equal [5, 2, 6, 7, 8], ArrayScopeListMixin.where(:parent_id => 2,:parent_type => 'something').order(:pos).map(&:id)
734
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:id)
735
+ assert_equal [5, 6, 7, 8], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').order('pos').map(&:id)
736
+ ArrayScopeListMixin.find(2).update :parent_id => 2, :pos => 2,:parent_type => 'something'
737
+ assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order('pos').map(&:id)
738
+ assert_equal [5, 2, 6, 7, 8], ArrayScopeListMixin.where(:parent_id => 2,:parent_type => 'something').order('pos').map(&:id)
564
739
  end
565
740
 
566
741
  def test_position_after_all_scope_properties_are_changed
567
742
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').map(&:pos)
568
743
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').map(&:pos)
569
- ArrayScopeListMixin.find(4).update_attributes(:parent_id => 2, :pos => 2, :parent_type => 'something')
570
- assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order(:pos).map(&:pos)
571
- assert_equal [1, 2, 3, 4, 5], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').order(:pos).map(&:pos)
744
+ ArrayScopeListMixin.find(4).update :parent_id => 2, :pos => 2, :parent_type => 'something'
745
+ assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:pos)
746
+ assert_equal [1, 2, 3, 4, 5], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').order('pos').map(&:pos)
572
747
  end
573
748
 
574
749
  def test_order_after_one_scope_property_is_changed
575
- assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order(:pos).map(&:id)
576
- assert_equal [9, 10, 11, 12], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').order(:pos).map(&:id)
577
- ArrayScopeListMixin.find(2).update_attributes(:parent_id => 3, :pos => 2)
578
- assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order(:pos).map(&:id)
579
- assert_equal [9, 2, 10, 11, 12], ArrayScopeListMixin.where(:parent_id => 3,:parent_type => 'anything').order(:pos).map(&:id)
750
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:id)
751
+ assert_equal [9, 10, 11, 12], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').order('pos').map(&:id)
752
+ ArrayScopeListMixin.find(2).update :parent_id => 3, :pos => 2
753
+ assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order('pos').map(&:id)
754
+ assert_equal [9, 2, 10, 11, 12], ArrayScopeListMixin.where(:parent_id => 3,:parent_type => 'anything').order('pos').map(&:id)
580
755
  end
581
756
 
582
757
  def test_position_after_one_scope_property_is_changed
583
758
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').map(&:pos)
584
759
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').map(&:pos)
585
- ArrayScopeListMixin.find(4).update_attributes(:parent_id => 3, :pos => 2)
586
- assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order(:pos).map(&:pos)
587
- assert_equal [1, 2, 3, 4, 5], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').order(:pos).map(&:pos)
760
+ ArrayScopeListMixin.find(4).update :parent_id => 3, :pos => 2
761
+ assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:pos)
762
+ assert_equal [1, 2, 3, 4, 5], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').order('pos').map(&:pos)
588
763
  end
589
764
 
590
765
  def test_order_after_moving_to_empty_list
591
- assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order(:pos).map(&:id)
592
- assert_equal [], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order(:pos).map(&:id)
593
- ArrayScopeListMixin.find(2).update_attributes(:parent_id => 4, :pos => 1)
594
- assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order(:pos).map(&:id)
595
- assert_equal [2], ArrayScopeListMixin.where(:parent_id => 4,:parent_type => 'anything').order(:pos).map(&:id)
766
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:id)
767
+ assert_equal [], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order('pos').map(&:id)
768
+ ArrayScopeListMixin.find(2).update :parent_id => 4, :pos => 1
769
+ assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order('pos').map(&:id)
770
+ assert_equal [2], ArrayScopeListMixin.where(:parent_id => 4,:parent_type => 'anything').order('pos').map(&:id)
596
771
  end
597
772
 
598
773
  def test_position_after_moving_to_empty_list
599
774
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').map(&:pos)
600
775
  assert_equal [], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').map(&:pos)
601
- ArrayScopeListMixin.find(2).update_attributes(:parent_id => 4, :pos => 1)
602
- assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order(:pos).map(&:pos)
603
- assert_equal [1], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order(:pos).map(&:pos)
776
+ ArrayScopeListMixin.find(2).update :parent_id => 4, :pos => 1
777
+ assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:pos)
778
+ assert_equal [1], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order('pos').map(&:pos)
779
+ end
780
+ end
781
+
782
+ class ArrayScopeListWithHashTest
783
+ def setup
784
+ setup_db
785
+ @obj1 = ArrayScopeListWithHashMixin.create! :pos => counter, :parent_id => 1, :state => nil
786
+ @obj2 = ArrayScopeListWithHashMixin.create! :pos => counter, :parent_id => 1, :state => 'anything'
787
+ end
788
+
789
+ def test_scope_condition_correct
790
+ [@obj1, @obj2].each do |obj|
791
+ assert_equal({ :parent_id => 1, :state => nil }, obj.scope_condition)
792
+ end
793
+ end
794
+ end
795
+
796
+ require 'timecop'
797
+
798
+ class TouchTest < ActsAsListTestCase
799
+ def setup
800
+ setup_db
801
+ Timecop.freeze(yesterday) do
802
+ 4.times { ListMixin.create! }
803
+ end
804
+ end
805
+
806
+ def now
807
+ @now ||= Time.current.change(usec: 0)
808
+ end
809
+
810
+ def yesterday
811
+ @yesterday ||= 1.day.ago
812
+ end
813
+
814
+ def updated_ats
815
+ ListMixin.order(:id).pluck(:updated_at)
816
+ end
817
+
818
+ def test_moving_item_lower_touches_self_and_lower_item
819
+ Timecop.freeze(now) do
820
+ ListMixin.first.move_lower
821
+ updated_ats[0..1].each do |updated_at|
822
+ assert_equal updated_at.to_i, now.to_i
823
+ end
824
+ updated_ats[2..3].each do |updated_at|
825
+ assert_equal updated_at.to_i, yesterday.to_i
826
+ end
827
+ end
828
+ end
829
+
830
+ def test_moving_item_higher_touches_self_and_higher_item
831
+ Timecop.freeze(now) do
832
+ ListMixin.all.second.move_higher
833
+ updated_ats[0..1].each do |updated_at|
834
+ assert_equal updated_at.to_i, now.to_i
835
+ end
836
+ updated_ats[2..3].each do |updated_at|
837
+ assert_equal updated_at.to_i, yesterday.to_i
838
+ end
839
+ end
840
+ end
841
+
842
+ def test_moving_item_to_bottom_touches_all_other_items
843
+ Timecop.freeze(now) do
844
+ ListMixin.first.move_to_bottom
845
+ updated_ats.each do |updated_at|
846
+ assert_equal updated_at.to_i, now.to_i
847
+ end
848
+ end
849
+ end
850
+
851
+ def test_moving_item_to_top_touches_all_other_items
852
+ Timecop.freeze(now) do
853
+ ListMixin.last.move_to_top
854
+ updated_ats.each do |updated_at|
855
+ assert_equal updated_at.to_i, now.to_i
856
+ end
857
+ end
858
+ end
859
+
860
+ def test_removing_item_touches_all_lower_items
861
+ Timecop.freeze(now) do
862
+ ListMixin.all.third.remove_from_list
863
+ updated_ats[0..1].each do |updated_at|
864
+ assert_equal updated_at.to_i, yesterday.to_i
865
+ end
866
+ updated_ats[2..2].each do |updated_at|
867
+ assert_equal updated_at.to_i, now.to_i
868
+ end
869
+ end
870
+ end
871
+ end
872
+
873
+ class TouchDisabledTest < ActsAsListTestCase
874
+ def setup
875
+ setup_db
876
+ Timecop.freeze(yesterday) do
877
+ 4.times { TouchDisabledMixin.create! }
878
+ end
879
+ end
880
+
881
+ def now
882
+ @now ||= Time.current.change(usec: 0)
883
+ end
884
+
885
+ def yesterday
886
+ @yesterday ||= 1.day.ago
887
+ end
888
+
889
+ def updated_ats
890
+ TouchDisabledMixin.order(:id).pluck(:updated_at)
891
+ end
892
+
893
+ def positions
894
+ ListMixin.order(:id).pluck(:pos)
895
+ end
896
+
897
+ def test_deleting_item_does_not_touch_higher_items
898
+ Timecop.freeze(now) do
899
+ TouchDisabledMixin.first.destroy
900
+ updated_ats.each do |updated_at|
901
+ assert_equal updated_at.to_i, yesterday.to_i
902
+ end
903
+ assert_equal positions, [1, 2, 3]
904
+ end
905
+ end
906
+ end
907
+
908
+ class ActsAsListTopTest < ActsAsListTestCase
909
+ def setup
910
+ setup_db
911
+ end
912
+
913
+ def test_acts_as_list_top
914
+ assert_equal 1, TheBaseSubclass.new.acts_as_list_top
915
+ assert_equal 0, ZeroBasedMixin.new.acts_as_list_top
916
+ end
917
+
918
+ def test_class_acts_as_list_top
919
+ assert_equal 1, TheBaseSubclass.acts_as_list_top
920
+ assert_equal 0, ZeroBasedMixin.acts_as_list_top
921
+ end
922
+ end
923
+
924
+ class NilPositionTest < ActsAsListTestCase
925
+ def setup
926
+ setup_db
927
+ end
928
+
929
+ def test_nil_position_ordering
930
+ new1 = DefaultScopedMixin.create pos: nil
931
+ new2 = DefaultScopedMixin.create pos: nil
932
+ new3 = DefaultScopedMixin.create pos: nil
933
+ DefaultScopedMixin.update_all(pos: nil)
934
+
935
+ assert_equal [nil, nil, nil], DefaultScopedMixin.all.map(&:pos)
936
+
937
+ new1.reload.pos = 1
938
+ new1.save
939
+
940
+ new3.reload.pos = 1
941
+ new3.save
942
+
943
+ assert_equal [1, 2], DefaultScopedMixin.where("pos IS NOT NULL").map(&:pos)
944
+ assert_equal [3, 1], DefaultScopedMixin.where("pos IS NOT NULL").map(&:id)
945
+ assert_nil new2.reload.pos
946
+
947
+ new2.reload.pos = 1
948
+ new2.save
949
+
950
+ assert_equal [1, 2, 3], DefaultScopedMixin.all.map(&:pos)
951
+ assert_equal [2, 3, 1], DefaultScopedMixin.all.map(&:id)
952
+ end
953
+ end
954
+
955
+ class SequentialUpdatesOptionDefaultTest < ActsAsListTestCase
956
+ def setup
957
+ setup_db
958
+ end
959
+
960
+ def test_sequential_updates_default_to_false_without_unique_index
961
+ assert_equal false, SequentialUpdatesDefault.new.send(:sequential_updates?)
962
+ end
963
+ end
964
+
965
+ class SequentialUpdatesMixinNotNullUniquePositiveConstraintsTest < ActsAsListTestCase
966
+ def setup
967
+ setup_db null: false, unique: true, positive: true
968
+ (1..4).each { |counter| SequentialUpdatesDefault.create!({pos: counter}) }
969
+ end
970
+
971
+ def test_sequential_updates_default_to_true_with_unique_index
972
+ assert_equal true, SequentialUpdatesDefault.new.send(:sequential_updates?)
973
+ end
974
+
975
+ def test_sequential_updates_option_override_with_false
976
+ assert_equal false, SequentialUpdatesFalseMixin.new.send(:sequential_updates?)
977
+ end
978
+
979
+ def test_insert_at
980
+ new = SequentialUpdatesDefault.create
981
+ assert_equal 5, new.pos
982
+
983
+ new.insert_at(1)
984
+ assert_equal 1, new.pos
985
+
986
+ new.insert_at(5)
987
+ assert_equal 5, new.pos
988
+
989
+ new.insert_at(3)
990
+ assert_equal 3, new.pos
991
+ end
992
+
993
+ def test_move_to_bottom
994
+ item = SequentialUpdatesDefault.order(:pos).first
995
+ item.move_to_bottom
996
+ assert_equal 4, item.pos
997
+ end
998
+
999
+ def test_move_to_top
1000
+ new_item = SequentialUpdatesDefault.create!
1001
+ assert_equal 5, new_item.pos
1002
+
1003
+ new_item.move_to_top
1004
+ assert_equal 1, new_item.pos
1005
+ end
1006
+
1007
+ def test_destroy
1008
+ new_item = SequentialUpdatesDefault.create
1009
+ assert_equal 5, new_item.pos
1010
+
1011
+ new_item.insert_at(2)
1012
+ assert_equal 2, new_item.pos
1013
+
1014
+ new_item.destroy
1015
+ assert_equal [1,2,3,4], SequentialUpdatesDefault.all.map(&:pos).sort
1016
+
1017
+ end
1018
+
1019
+ def test_exception_on_wrong_position
1020
+ new_item = SequentialUpdatesDefault.create
1021
+
1022
+ assert_raises ArgumentError do
1023
+ new_item.insert_at(0)
1024
+ end
1025
+ end
1026
+
1027
+
1028
+ class SequentialUpdatesMixinNotNullUniquePositiveConstraintsTest < ActsAsListTestCase
1029
+ def setup
1030
+ setup_db null: false, unique: true, positive: true
1031
+ (1..4).each { |counter| SequentialUpdatesAltId.create!({pos: counter}) }
1032
+ end
1033
+
1034
+ def test_sequential_updates_default_to_true_with_unique_index
1035
+ assert_equal true, SequentialUpdatesAltId.new.send(:sequential_updates?)
1036
+ end
1037
+
1038
+ def test_insert_at
1039
+ new = SequentialUpdatesAltId.create
1040
+ assert_equal 5, new.pos
1041
+
1042
+ new.insert_at(1)
1043
+ assert_equal 1, new.pos
1044
+
1045
+ new.insert_at(5)
1046
+ assert_equal 5, new.pos
1047
+
1048
+ new.insert_at(3)
1049
+ assert_equal 3, new.pos
1050
+ end
1051
+
1052
+ def test_create_at_top
1053
+ new = SequentialUpdatesAltId.create!(pos: 1)
1054
+ assert_equal 1, new.pos
1055
+ end
1056
+
1057
+ def test_move_to_bottom
1058
+ item = SequentialUpdatesAltId.order(:pos).first
1059
+ item.move_to_bottom
1060
+ assert_equal 4, item.pos
1061
+ end
1062
+
1063
+ def test_move_to_top
1064
+ new_item = SequentialUpdatesAltId.create!
1065
+ assert_equal 5, new_item.pos
1066
+
1067
+ new_item.move_to_top
1068
+ assert_equal 1, new_item.pos
1069
+ end
1070
+
1071
+ def test_destroy
1072
+ new_item = SequentialUpdatesAltId.create
1073
+ assert_equal 5, new_item.pos
1074
+
1075
+ new_item.insert_at(2)
1076
+ assert_equal 2, new_item.pos
1077
+
1078
+ new_item.destroy
1079
+ assert_equal [1,2,3,4], SequentialUpdatesAltId.all.map(&:pos).sort
1080
+
1081
+ end
1082
+ end
1083
+
1084
+ class SequentialUpdatesAltIdTouchDisabledTest < ActsAsListTestCase
1085
+ def setup
1086
+ setup_db
1087
+ Timecop.freeze(yesterday) do
1088
+ 4.times { SequentialUpdatesAltIdTouchDisabled.create! }
1089
+ end
1090
+ end
1091
+
1092
+ def now
1093
+ @now ||= Time.current.change(usec: 0)
1094
+ end
1095
+
1096
+ def yesterday
1097
+ @yesterday ||= 1.day.ago
1098
+ end
1099
+
1100
+ def updated_ats
1101
+ SequentialUpdatesAltIdTouchDisabled.order(:altid).pluck(:updated_at)
1102
+ end
1103
+
1104
+ def positions
1105
+ SequentialUpdatesAltIdTouchDisabled.order(:altid).pluck(:pos)
1106
+ end
1107
+
1108
+ def test_sequential_updates_default_to_true_with_unique_index
1109
+ assert_equal true, SequentialUpdatesAltIdTouchDisabled.new.send(:sequential_updates?)
1110
+ end
1111
+
1112
+ def test_deleting_item_does_not_touch_higher_items
1113
+ Timecop.freeze(now) do
1114
+ SequentialUpdatesAltIdTouchDisabled.first.destroy
1115
+ updated_ats.each do |updated_at|
1116
+ assert_equal updated_at.to_i, yesterday.to_i
1117
+ end
1118
+ assert_equal positions, [1, 2, 3]
1119
+ end
1120
+ end
604
1121
  end
605
1122
  end