acts_as_list 0.7.4 → 1.1.0

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