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
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Acts
5
+ module List
6
+ module NoUpdate
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ class ArrayTypeError < ArgumentError
13
+ def initialize
14
+ super("The first argument must be an array")
15
+ end
16
+ end
17
+
18
+ class DisparityClassesError < ArgumentError
19
+ def initialize
20
+ super("The first argument should contain ActiveRecord or ApplicationRecord classes")
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ # Lets you selectively disable all act_as_list database updates
26
+ # for the duration of a block.
27
+ #
28
+ # ==== Examples
29
+ #
30
+ # class TodoList < ActiveRecord::Base
31
+ # has_many :todo_items, -> { order(position: :asc) }
32
+ # end
33
+ #
34
+ # class TodoItem < ActiveRecord::Base
35
+ # belongs_to :todo_list
36
+ #
37
+ # acts_as_list scope: :todo_list
38
+ # end
39
+ #
40
+ # TodoItem.acts_as_list_no_update do
41
+ # TodoList.first.update(position: 2)
42
+ # end
43
+ #
44
+ # You can also pass an array of classes as an argument to disable database updates on just those classes.
45
+ # It can be any ActiveRecord class that has acts_as_list enabled.
46
+ #
47
+ # ==== Examples
48
+ #
49
+ # class TodoList < ActiveRecord::Base
50
+ # has_many :todo_items, -> { order(position: :asc) }
51
+ # acts_as_list
52
+ # end
53
+ #
54
+ # class TodoItem < ActiveRecord::Base
55
+ # belongs_to :todo_list
56
+ # has_many :todo_attachments, -> { order(position: :asc) }
57
+ #
58
+ # acts_as_list scope: :todo_list
59
+ # end
60
+ #
61
+ # class TodoAttachment < ActiveRecord::Base
62
+ # belongs_to :todo_list
63
+ # acts_as_list scope: :todo_item
64
+ # end
65
+ #
66
+ # TodoItem.acts_as_list_no_update([TodoAttachment]) do
67
+ # TodoItem.find(10).update(position: 2)
68
+ # TodoAttachment.find(10).update(position: 1)
69
+ # TodoAttachment.find(11).update(position: 2)
70
+ # TodoList.find(2).update(position: 3) # For this instance the callbacks will be called because we haven't passed the class as an argument
71
+ # end
72
+
73
+ def acts_as_list_no_update(extra_classes = [], &block)
74
+ return raise ArrayTypeError unless extra_classes.is_a?(Array)
75
+
76
+ extra_classes << self
77
+
78
+ return raise DisparityClassesError unless active_record_objects?(extra_classes)
79
+
80
+ NoUpdate.apply_to(extra_classes, &block)
81
+ end
82
+
83
+ private
84
+
85
+ def active_record_objects?(extra_classes)
86
+ extra_classes.all? { |klass| klass.ancestors.include? ActiveRecord::Base }
87
+ end
88
+ end
89
+
90
+ class << self
91
+ def apply_to(klasses)
92
+ klasses.map {|klass| add_klass(klass)}
93
+ yield
94
+ ensure
95
+ klasses.map {|klass| remove_klass(klass)}
96
+ end
97
+
98
+ def applied_to?(klass)
99
+ !(klass.ancestors & extracted_klasses.keys).empty?
100
+ end
101
+
102
+ private
103
+
104
+ def extracted_klasses
105
+ Thread.current[:act_as_list_no_update] ||= {}
106
+ end
107
+
108
+ def add_klass(klass)
109
+ extracted_klasses[klass] = 0 unless extracted_klasses.key?(klass)
110
+ extracted_klasses[klass] += 1
111
+ end
112
+
113
+ def remove_klass(klass)
114
+ extracted_klasses[klass] -= 1
115
+ extracted_klasses.delete(klass) if extracted_klasses[klass] <= 0
116
+ end
117
+ end
118
+
119
+ def act_as_list_no_update?
120
+ NoUpdate.applied_to?(self.class)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
4
+ def self.call(caller_class, position_column, touch_on_update)
5
+ define_class_methods(caller_class, position_column, touch_on_update)
6
+ define_instance_methods(caller_class, position_column)
7
+
8
+ if mass_assignment_protection_was_used_by_user?(caller_class)
9
+ protect_attributes_from_mass_assignment(caller_class, position_column)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def self.define_class_methods(caller_class, position_column, touch_on_update)
16
+ caller_class.class_eval do
17
+ define_singleton_method :quoted_position_column do
18
+ @_quoted_position_column ||= connection.quote_column_name(position_column)
19
+ end
20
+
21
+ define_singleton_method :quoted_position_column_with_table_name do
22
+ @_quoted_position_column_with_table_name ||= "#{caller_class.quoted_table_name}.#{quoted_position_column}"
23
+ end
24
+
25
+ define_singleton_method :decrement_sequentially do
26
+ pluck(primary_key).each do |id|
27
+ where(primary_key => id).decrement_all
28
+ end
29
+ end
30
+
31
+ define_singleton_method :increment_sequentially do
32
+ pluck(primary_key).each do |id|
33
+ where(primary_key => id).increment_all
34
+ end
35
+ end
36
+
37
+ define_singleton_method :decrement_all do
38
+ update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} - 1)"
39
+ end
40
+
41
+ define_singleton_method :increment_all do
42
+ update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} + 1)"
43
+ end
44
+
45
+ define_singleton_method :update_all_with_touch do |updates|
46
+ updates += touch_record_sql if touch_on_update
47
+ update_all(updates)
48
+ end
49
+
50
+ private
51
+
52
+ define_singleton_method :touch_record_sql do
53
+ new.touch_record_sql
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.define_instance_methods(caller_class, position_column)
59
+ caller_class.class_eval do
60
+ attr_reader :position_changed
61
+
62
+ define_method :position_column do
63
+ position_column
64
+ end
65
+
66
+ define_method :"#{position_column}=" do |position|
67
+ self[position_column] = position
68
+ @position_changed = true
69
+ end
70
+
71
+ define_method :touch_record_sql do
72
+ cached_quoted_now = quoted_current_time_from_proper_timezone
73
+
74
+ timestamp_attributes_for_update_in_model.map do |attr|
75
+ ", #{connection.quote_column_name(attr)} = #{cached_quoted_now}"
76
+ end.join
77
+ end
78
+
79
+ private
80
+
81
+ delegate :connection, to: self
82
+
83
+ def quoted_current_time_from_proper_timezone
84
+ connection.quote(connection.quoted_date(
85
+ current_time_from_proper_timezone))
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.mass_assignment_protection_was_used_by_user?(caller_class)
91
+ caller_class.class_eval do
92
+ respond_to?(:accessible_attributes) and accessible_attributes.present?
93
+ end
94
+ end
95
+
96
+ def self.protect_attributes_from_mass_assignment(caller_class, position_column)
97
+ caller_class.class_eval do
98
+ attr_accessible position_column.to_sym
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require "active_support/inflector"
3
+
4
+ module ActiveRecord::Acts::List::ScopeMethodDefiner #:nodoc:
5
+ extend ActiveSupport::Inflector
6
+
7
+ def self.call(caller_class, scope)
8
+ scope = idify(caller_class, scope) if scope.is_a?(Symbol)
9
+
10
+ caller_class.class_eval do
11
+ define_method :scope_name do
12
+ scope
13
+ end
14
+
15
+ if scope.is_a?(Symbol)
16
+ define_method :scope_condition do
17
+ { scope => send(:"#{scope}") }
18
+ end
19
+
20
+ define_method :scope_changed? do
21
+ changed.include?(scope_name.to_s)
22
+ end
23
+
24
+ define_method :destroyed_via_scope? do
25
+ scope == (destroyed_by_association && destroyed_by_association.foreign_key.to_sym)
26
+ end
27
+ elsif scope.is_a?(Array)
28
+ define_method :scope_condition do
29
+ # The elements of the Array can be symbols, strings, or hashes.
30
+ # If symbols or strings, they are treated as column names and the current value is looked up.
31
+ # If hashes, they are treated as fixed values.
32
+ scope.inject({}) do |hash, column_or_fixed_vals|
33
+ if column_or_fixed_vals.is_a?(Hash)
34
+ fixed_vals = column_or_fixed_vals
35
+ hash.merge!(fixed_vals)
36
+ else
37
+ column = column_or_fixed_vals
38
+ hash.merge!({ column.to_sym => read_attribute(column.to_sym) })
39
+ end
40
+ end
41
+ end
42
+
43
+ define_method :scope_changed? do
44
+ (scope_condition.keys & changed.map(&:to_sym)).any?
45
+ end
46
+
47
+ define_method :destroyed_via_scope? do
48
+ scope_condition.keys.include? (destroyed_by_association && destroyed_by_association.foreign_key.to_sym)
49
+ end
50
+ else
51
+ define_method :scope_condition do
52
+ eval "%{#{scope}}"
53
+ end
54
+
55
+ define_method :scope_changed? do
56
+ false
57
+ end
58
+
59
+ define_method :destroyed_via_scope? do
60
+ false
61
+ end
62
+ end
63
+
64
+ self.scope :in_list, lambda { where("#{quoted_position_column_with_table_name} IS NOT NULL") }
65
+ end
66
+ end
67
+
68
+ def self.idify(caller_class, name)
69
+ return name if name.to_s =~ /_id$/
70
+
71
+ if caller_class.reflections.key?(name.to_s)
72
+ caller_class.reflections[name.to_s].foreign_key.to_sym
73
+ else
74
+ foreign_key(name).to_sym
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner #:nodoc:
4
+ def self.call(caller_class, column, sequential_updates_option)
5
+ caller_class.class_eval do
6
+ define_method :sequential_updates? do
7
+ if !defined?(@sequential_updates)
8
+ if sequential_updates_option.nil?
9
+ table_exists =
10
+ if active_record_version_is?('>= 5')
11
+ caller_class.connection.data_source_exists?(caller_class.table_name)
12
+ else
13
+ caller_class.connection.table_exists?(caller_class.table_name)
14
+ end
15
+ index_exists = caller_class.connection.index_exists?(caller_class.table_name, column, unique: true)
16
+ @sequential_updates = table_exists && index_exists
17
+ else
18
+ @sequential_updates = sequential_updates_option
19
+ end
20
+ else
21
+ @sequential_updates
22
+ end
23
+ end
24
+
25
+ private :sequential_updates?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Acts::List::TopOfListMethodDefiner #:nodoc:
4
+ def self.call(caller_class, top_of_list)
5
+ caller_class.class_eval do
6
+ define_singleton_method :acts_as_list_top do
7
+ top_of_list.to_i
8
+ end
9
+
10
+ define_method :acts_as_list_top do
11
+ top_of_list.to_i
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module Acts
3
5
  module List
4
- VERSION = '0.7.4'
6
+ VERSION = '1.1.0'
5
7
  end
6
8
  end
7
9
  end
data/lib/acts_as_list.rb CHANGED
@@ -1,15 +1,12 @@
1
- require 'acts_as_list/active_record/acts/list'
1
+ # frozen_string_literal: true
2
2
 
3
- module ActsAsList
4
- if defined?(Rails::Railtie)
5
- class Railtie < Rails::Railtie
6
- initializer 'acts_as_list.insert_into_active_record' do
7
- ActiveSupport.on_load :active_record do
8
- ActiveRecord::Base.send(:include, ActiveRecord::Acts::List)
9
- end
10
- end
11
- end
12
- else
13
- ActiveRecord::Base.send(:include, ActiveRecord::Acts::List) if defined?(ActiveRecord)
14
- end
15
- end
3
+ require "acts_as_list/active_record/acts/list"
4
+ require "acts_as_list/active_record/acts/position_column_method_definer"
5
+ require "acts_as_list/active_record/acts/scope_method_definer"
6
+ require "acts_as_list/active_record/acts/top_of_list_method_definer"
7
+ require "acts_as_list/active_record/acts/add_new_at_method_definer"
8
+ require "acts_as_list/active_record/acts/aux_method_definer"
9
+ require "acts_as_list/active_record/acts/callback_definer"
10
+ require "acts_as_list/active_record/acts/no_update"
11
+ require "acts_as_list/active_record/acts/sequential_updates_method_definer"
12
+ require "acts_as_list/active_record/acts/active_record"
data/test/database.yml ADDED
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ adapter: sqlite3
3
+ database: "file:memdb1?mode=memory&cache=shared"
4
+
5
+ mysql:
6
+ adapter: mysql2
7
+ host: 127.0.0.1
8
+ username: root
9
+ password:
10
+ database: acts_as_list
11
+
12
+ postgresql:
13
+ adapter: postgresql
14
+ host: localhost
15
+ username: postgres
16
+ password: postgres
17
+ database: acts_as_list
18
+ min_messages: ERROR
data/test/helper.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # $DEBUG = true
4
+
1
5
  require "rubygems"
2
6
  require "bundler/setup"
3
7
  begin
@@ -9,13 +13,57 @@ rescue Bundler::BundlerError => e
9
13
  end
10
14
  require "active_record"
11
15
  require "minitest/autorun"
16
+ require "mocha/minitest"
12
17
  require "#{File.dirname(__FILE__)}/../init"
13
18
 
14
19
  if defined?(ActiveRecord::VERSION) &&
15
- ActiveRecord::VERSION::MAJOR > 4 ||
16
- (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2)
20
+ ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2
17
21
 
22
+ # Was removed in Rails 5 and is effectively true.
18
23
  ActiveRecord::Base.raise_in_transactional_callbacks = true
19
24
  end
20
25
 
26
+ db_config = YAML.load_file(File.expand_path("../database.yml", __FILE__)).fetch(ENV["DB"] || "sqlite")
27
+ ActiveRecord::Base.establish_connection(db_config)
28
+ ActiveRecord::Schema.verbose = false
29
+
30
+ def teardown_db
31
+ if ActiveRecord::VERSION::MAJOR >= 5
32
+ tables = ActiveRecord::Base.connection.data_sources
33
+ else
34
+ tables = ActiveRecord::Base.connection.tables
35
+ end
36
+
37
+ tables.each do |table|
38
+ ActiveRecord::Base.connection.drop_table(table)
39
+ end
40
+ end
41
+
21
42
  require "shared"
43
+
44
+ # require 'logger'
45
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
46
+
47
+ def assert_equal_or_nil(a, b)
48
+ if a.nil?
49
+ assert_nil b
50
+ else
51
+ assert_equal a, b
52
+ end
53
+ end
54
+
55
+ def assert_no_deprecation_warning_raised_by(failure_message = 'ActiveRecord deprecation warning raised when we didn\'t expect it', pass_message = 'No ActiveRecord deprecation raised')
56
+ original_behavior = ActiveSupport::Deprecation.behavior
57
+ ActiveSupport::Deprecation.behavior = :raise
58
+ begin
59
+ yield
60
+ rescue ActiveSupport::DeprecationException => e
61
+ flunk "#{failure_message}: #{e}"
62
+ rescue
63
+ raise
64
+ else
65
+ pass pass_message
66
+ end
67
+ ensure
68
+ ActiveSupport::Deprecation.behavior = original_behavior
69
+ end
data/test/shared.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Common shared behaviour.
2
4
  module Shared
3
5
  autoload :List, 'shared_list'
@@ -6,4 +8,5 @@ module Shared
6
8
  autoload :ArrayScopeList, 'shared_array_scope_list'
7
9
  autoload :TopAddition, 'shared_top_addition'
8
10
  autoload :NoAddition, 'shared_no_addition'
11
+ autoload :Quoting, 'shared_quoting'
9
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shared
2
4
  module ArrayScopeList
3
5
  def setup
@@ -25,7 +27,7 @@ module Shared
25
27
 
26
28
  ArrayScopeListMixin.where(id: 4).first.move_to_top
27
29
  assert_equal [4, 1, 3, 2], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
28
-
30
+
29
31
  ArrayScopeListMixin.where(id: 4).first.insert_at(4)
30
32
  assert_equal [1, 3, 2, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
31
33
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:pos)
@@ -60,6 +62,11 @@ module Shared
60
62
  assert !new.first?
61
63
  assert new.last?
62
64
 
65
+ new = ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass') }
66
+ assert_equal_or_nil $default_position,new.pos
67
+ assert_equal $default_position.is_a?(Integer), new.first?
68
+ assert !new.last?
69
+
63
70
  new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
64
71
  assert_equal 3, new.pos
65
72
  assert !new.first?
@@ -81,6 +88,9 @@ module Shared
81
88
  new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
82
89
  assert_equal 3, new.pos
83
90
 
91
+ new_noup = ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass') }
92
+ assert_equal_or_nil $default_position,new_noup.pos
93
+
84
94
  new4 = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
85
95
  assert_equal 4, new4.pos
86
96
 
@@ -104,6 +114,9 @@ module Shared
104
114
 
105
115
  new4.reload
106
116
  assert_equal 5, new4.pos
117
+
118
+ new_noup.reload
119
+ assert_equal_or_nil $default_position, new_noup.pos
107
120
  end
108
121
 
109
122
  def test_delete_middle
@@ -123,6 +136,12 @@ module Shared
123
136
 
124
137
  assert_equal 1, ArrayScopeListMixin.where(id: 3).first.pos
125
138
  assert_equal 2, ArrayScopeListMixin.where(id: 4).first.pos
139
+
140
+ ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.where(id: 3).first.destroy }
141
+
142
+ assert_equal [4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
143
+
144
+ assert_equal 2, ArrayScopeListMixin.where(id: 4).first.pos
126
145
  end
127
146
 
128
147
  def test_remove_from_list_should_then_fail_in_list?
@@ -136,10 +155,8 @@ module Shared
136
155
 
137
156
  ArrayScopeListMixin.where(id: 2).first.remove_from_list
138
157
 
139
- assert_equal [2, 1, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
140
-
141
158
  assert_equal 1, ArrayScopeListMixin.where(id: 1).first.pos
142
- assert_equal nil, ArrayScopeListMixin.where(id: 2).first.pos
159
+ assert_nil ArrayScopeListMixin.where(id: 2).first.pos
143
160
  assert_equal 2, ArrayScopeListMixin.where(id: 3).first.pos
144
161
  assert_equal 3, ArrayScopeListMixin.where(id: 4).first.pos
145
162
  end