acts_as_list 0.7.2 → 0.7.5

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.
@@ -38,32 +38,30 @@ module ActiveRecord
38
38
  configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom}
39
39
  configuration.update(options) if options.is_a?(Hash)
40
40
 
41
- configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
41
+ if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
42
+ configuration[:scope] = :"#{configuration[:scope]}_id"
43
+ end
42
44
 
43
45
  if configuration[:scope].is_a?(Symbol)
44
46
  scope_methods = %(
45
47
  def scope_condition
46
- { :#{configuration[:scope].to_s} => send(:#{configuration[:scope].to_s}) }
48
+ { #{configuration[:scope]}: send(:#{configuration[:scope]}) }
47
49
  end
48
50
 
49
51
  def scope_changed?
50
- changes.include?(scope_name.to_s)
52
+ changed.include?(scope_name.to_s)
51
53
  end
52
54
  )
53
55
  elsif configuration[:scope].is_a?(Array)
54
56
  scope_methods = %(
55
- def attrs
56
- %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
57
- memo[column.intern] = read_attribute(column.intern); memo
57
+ def scope_condition
58
+ #{configuration[:scope]}.inject({}) do |hash, column|
59
+ hash.merge!({ column.to_sym => read_attribute(column.to_sym) })
58
60
  end
59
61
  end
60
62
 
61
63
  def scope_changed?
62
- (attrs.keys & changes.keys.map(&:to_sym)).any?
63
- end
64
-
65
- def scope_condition
66
- attrs
64
+ (scope_condition.keys & changed.map(&:to_sym)).any?
67
65
  end
68
66
  )
69
67
  else
@@ -76,9 +74,9 @@ module ActiveRecord
76
74
  )
77
75
  end
78
76
 
79
- class_eval <<-EOV
80
- include ::ActiveRecord::Acts::List::InstanceMethods
77
+ quoted_position_column = connection.quote_column_name(configuration[:column])
81
78
 
79
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
82
80
  def acts_as_list_top
83
81
  #{configuration[:top_of_list]}.to_i
84
82
  end
@@ -113,19 +111,46 @@ module ActiveRecord
113
111
  attr_accessible :#{configuration[:column]}
114
112
  end
115
113
 
116
- before_destroy :reload_position
117
- after_destroy :decrement_positions_on_lower_items
118
- before_update :check_scope
119
- after_update :update_positions
120
- before_validation :check_top_position
114
+ scope :in_list, lambda { where(%q{#{quoted_table_name}.#{quoted_position_column} IS NOT NULL}) }
115
+
116
+ def self.decrement_all
117
+ update_all_with_touch %q(#{quoted_position_column} = (#{quoted_table_name}.#{quoted_position_column} - 1))
118
+ end
119
+
120
+ def self.increment_all
121
+ update_all_with_touch %q(#{quoted_position_column} = (#{quoted_table_name}.#{quoted_position_column} + 1))
122
+ end
123
+
124
+ def self.update_all_with_touch(updates)
125
+ record = new
126
+ attrs = record.send(:timestamp_attributes_for_update_in_model)
127
+ now = record.send(:current_time_from_proper_timezone)
128
+
129
+ query = attrs.map { |attr| %(\#{connection.quote_column_name(attr)} = :now) }
130
+ query.push updates
131
+ query = query.join(", ")
121
132
 
122
- scope :in_list, lambda { where("#{table_name}.#{configuration[:column]} IS NOT NULL") }
133
+ update_all([query, now: now])
134
+ end
123
135
  EOV
124
136
 
137
+ attr_reader :position_changed
138
+
139
+ before_validation :check_top_position
140
+
141
+ before_destroy :lock!
142
+ after_destroy :decrement_positions_on_lower_items
143
+
144
+ before_update :check_scope
145
+ after_update :update_positions
146
+
147
+ after_commit :clear_scope_changed
148
+
125
149
  if configuration[:add_new_at].present?
126
- self.send(:before_create, "add_to_list_#{configuration[:add_new_at]}")
150
+ before_create "add_to_list_#{configuration[:add_new_at]}".to_sym
127
151
  end
128
152
 
153
+ include ::ActiveRecord::Acts::List::InstanceMethods
129
154
  end
130
155
  end
131
156
 
@@ -221,10 +246,7 @@ module ActiveRecord
221
246
  # Return the next higher item in the list.
222
247
  def higher_item
223
248
  return nil unless in_list?
224
- acts_as_list_class.unscoped do
225
- acts_as_list_class.where(scope_condition).where("#{position_column} < #{(send(position_column).to_i).to_s}").
226
- order("#{acts_as_list_class.table_name}.#{position_column} DESC").first
227
- end
249
+ higher_items(1).first
228
250
  end
229
251
 
230
252
  # Return the next n higher items in the list
@@ -233,19 +255,16 @@ module ActiveRecord
233
255
  limit ||= acts_as_list_list.count
234
256
  position_value = send(position_column)
235
257
  acts_as_list_list.
236
- where("#{position_column} < ?", position_value).
237
- where("#{position_column} >= ?", position_value - limit).
258
+ where("#{quoted_table_name}.#{quoted_position_column} < ?", position_value).
259
+ where("#{quoted_table_name}.#{quoted_position_column} >= ?", position_value - limit).
238
260
  limit(limit).
239
- order("#{acts_as_list_class.table_name}.#{position_column} ASC")
261
+ order("#{quoted_table_name}.#{quoted_position_column} ASC")
240
262
  end
241
263
 
242
264
  # Return the next lower item in the list.
243
265
  def lower_item
244
266
  return nil unless in_list?
245
- acts_as_list_class.unscoped do
246
- acts_as_list_class.where(scope_condition).where("#{position_column} > #{(send(position_column).to_i).to_s}").
247
- order("#{acts_as_list_class.table_name}.#{position_column} ASC").first
248
- end
267
+ lower_items(1).first
249
268
  end
250
269
 
251
270
  # Return the next n lower items in the list
@@ -254,10 +273,10 @@ module ActiveRecord
254
273
  limit ||= acts_as_list_list.count
255
274
  position_value = send(position_column)
256
275
  acts_as_list_list.
257
- where("#{position_column} > ?", position_value).
258
- where("#{position_column} <= ?", position_value + limit).
276
+ where("#{quoted_table_name}.#{quoted_position_column} > ?", position_value).
277
+ where("#{quoted_table_name}.#{quoted_position_column} <= ?", position_value + limit).
259
278
  limit(limit).
260
- order("#{acts_as_list_class.table_name}.#{position_column} ASC")
279
+ order("#{quoted_table_name}.#{quoted_position_column} ASC")
261
280
  end
262
281
 
263
282
  # Test if this record is in a list
@@ -293,14 +312,26 @@ module ActiveRecord
293
312
  def add_to_list_top
294
313
  increment_positions_on_all_items
295
314
  self[position_column] = acts_as_list_top
315
+ # Make sure we know that we've processed this scope change already
316
+ @scope_changed = false
317
+ #dont halt the callback chain
318
+ true
296
319
  end
297
320
 
321
+ # A poorly named method. It will insert the item at the desired position if the position
322
+ # has been set manually using position=, not necessarily the bottom of the list
298
323
  def add_to_list_bottom
299
- if not_in_list? || scope_changed? && !@position_changed || default_position?
324
+ if not_in_list? || internal_scope_changed? && !position_changed || default_position?
300
325
  self[position_column] = bottom_position_in_list.to_i + 1
301
326
  else
302
327
  increment_positions_on_lower_items(self[position_column], id)
303
328
  end
329
+
330
+ # Make sure we know that we've processed this scope change already
331
+ @scope_changed = false
332
+
333
+ #dont halt the callback chain
334
+ true
304
335
  end
305
336
 
306
337
  # Overwrite this method to define the scope of the list changes
@@ -315,11 +346,12 @@ module ActiveRecord
315
346
 
316
347
  # Returns the bottom item
317
348
  def bottom_item(except = nil)
318
- conditions = scope_condition
319
349
  conditions = except ? "#{self.class.primary_key} != #{self.class.connection.quote(except.id)}" : {}
320
- acts_as_list_class.unscoped do
321
- acts_as_list_class.in_list.where(scope_condition).where(conditions).order("#{acts_as_list_class.table_name}.#{position_column} DESC").first
322
- end
350
+ acts_as_list_list.in_list.where(
351
+ conditions
352
+ ).order(
353
+ "#{quoted_table_name}.#{quoted_position_column} DESC"
354
+ ).first
323
355
  end
324
356
 
325
357
  # Forces item to assume the bottom position in the list.
@@ -334,62 +366,32 @@ module ActiveRecord
334
366
 
335
367
  # This has the effect of moving all the higher items up one.
336
368
  def decrement_positions_on_higher_items(position)
337
- acts_as_list_class.unscoped do
338
- acts_as_list_class.where(scope_condition).where(
339
- "#{position_column} <= #{position}"
340
- ).update_all(
341
- "#{position_column} = (#{position_column} - 1)"
342
- )
343
- end
369
+ acts_as_list_list.where("#{quoted_position_column} <= #{position}").decrement_all
344
370
  end
345
371
 
346
372
  # This has the effect of moving all the lower items up one.
347
373
  def decrement_positions_on_lower_items(position=nil)
348
374
  return unless in_list?
349
375
  position ||= send(position_column).to_i
350
- acts_as_list_class.unscoped do
351
- acts_as_list_class.where(scope_condition).where(
352
- "#{position_column} > #{position}"
353
- ).update_all(
354
- "#{position_column} = (#{position_column} - 1)"
355
- )
356
- end
376
+ acts_as_list_list.where("#{quoted_position_column} > #{position}").decrement_all
357
377
  end
358
378
 
359
379
  # This has the effect of moving all the higher items down one.
360
380
  def increment_positions_on_higher_items
361
381
  return unless in_list?
362
- acts_as_list_class.unscoped do
363
- acts_as_list_class.where(scope_condition).where(
364
- "#{position_column} < #{send(position_column).to_i}"
365
- ).update_all(
366
- "#{position_column} = (#{position_column} + 1)"
367
- )
368
- end
382
+ acts_as_list_list.where("#{quoted_position_column} < #{send(position_column).to_i}").increment_all
369
383
  end
370
384
 
371
385
  # This has the effect of moving all the lower items down one.
372
386
  def increment_positions_on_lower_items(position, avoid_id = nil)
373
387
  avoid_id_condition = avoid_id ? " AND #{self.class.primary_key} != #{self.class.connection.quote(avoid_id)}" : ''
374
388
 
375
- acts_as_list_class.unscoped do
376
- acts_as_list_class.where(scope_condition).where(
377
- "#{position_column} >= #{position}#{avoid_id_condition}"
378
- ).update_all(
379
- "#{position_column} = (#{position_column} + 1)"
380
- )
381
- end
389
+ acts_as_list_list.where("#{quoted_position_column} >= #{position}#{avoid_id_condition}").increment_all
382
390
  end
383
391
 
384
392
  # Increments position (<tt>position_column</tt>) of all items in the list.
385
393
  def increment_positions_on_all_items
386
- acts_as_list_class.unscoped do
387
- acts_as_list_class.where(
388
- scope_condition
389
- ).update_all(
390
- "#{position_column} = (#{position_column} + 1)"
391
- )
392
- end
394
+ acts_as_list_list.increment_all
393
395
  end
394
396
 
395
397
  # Reorders intermediate items to support moving an item from old_position to new_position.
@@ -402,29 +404,21 @@ module ActiveRecord
402
404
  #
403
405
  # e.g., if moving an item from 2 to 5,
404
406
  # move [3, 4, 5] to [2, 3, 4]
405
- acts_as_list_class.unscoped do
406
- acts_as_list_class.where(scope_condition).where(
407
- "#{position_column} > #{old_position}"
408
- ).where(
409
- "#{position_column} <= #{new_position}#{avoid_id_condition}"
410
- ).update_all(
411
- "#{position_column} = (#{position_column} - 1)"
412
- )
413
- end
407
+ acts_as_list_list.where(
408
+ "#{quoted_position_column} > #{old_position}"
409
+ ).where(
410
+ "#{quoted_position_column} <= #{new_position}#{avoid_id_condition}"
411
+ ).decrement_all
414
412
  else
415
413
  # Increment position of intermediate items
416
414
  #
417
415
  # e.g., if moving an item from 5 to 2,
418
416
  # move [2, 3, 4] to [3, 4, 5]
419
- acts_as_list_class.unscoped do
420
- acts_as_list_class.where(scope_condition).where(
421
- "#{position_column} >= #{new_position}"
422
- ).where(
423
- "#{position_column} < #{old_position}#{avoid_id_condition}"
424
- ).update_all(
425
- "#{position_column} = (#{position_column} + 1)"
426
- )
427
- end
417
+ acts_as_list_list.where(
418
+ "#{quoted_position_column} >= #{new_position}"
419
+ ).where(
420
+ "#{quoted_position_column} < #{old_position}#{avoid_id_condition}"
421
+ ).increment_all
428
422
  end
429
423
  end
430
424
 
@@ -453,31 +447,34 @@ module ActiveRecord
453
447
  old_position = send("#{position_column}_was").to_i
454
448
  new_position = send(position_column).to_i
455
449
 
456
- return unless acts_as_list_class.unscoped do
457
- acts_as_list_class.where(scope_condition).where("#{position_column} = #{new_position}").count > 1
458
- end
450
+ return unless acts_as_list_list.where(
451
+ "#{quoted_position_column} = #{new_position}"
452
+ ).count > 1
459
453
  shuffle_positions_on_intermediate_items old_position, new_position, id
460
454
  end
461
455
 
462
- # Temporarily swap changes attributes with current attributes
463
- def swap_changed_attributes
464
- @changed_attributes.each { |k, _| @changed_attributes[k], self[k] =
465
- self[k], @changed_attributes[k] }
456
+ def internal_scope_changed?
457
+ return @scope_changed if defined?(@scope_changed)
458
+
459
+ @scope_changed = scope_changed?
460
+ end
461
+
462
+ def clear_scope_changed
463
+ remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
466
464
  end
467
465
 
468
466
  def check_scope
469
- if scope_changed?
470
- swap_changed_attributes
467
+ if internal_scope_changed?
468
+ cached_changes = changes
469
+
470
+ cached_changes.each { |attribute, values| self[attribute] = values[0] }
471
471
  send('decrement_positions_on_lower_items') if lower_item
472
- swap_changed_attributes
472
+ cached_changes.each { |attribute, values| self[attribute] = values[1] }
473
+
473
474
  send("add_to_list_#{add_new_at}")
474
475
  end
475
476
  end
476
477
 
477
- def reload_position
478
- self.reload
479
- end
480
-
481
478
  # This check is skipped if the position is currently the default position from the table
482
479
  # as modifying the default position on creation is handled elsewhere
483
480
  def check_top_position
@@ -485,6 +482,16 @@ module ActiveRecord
485
482
  self[position_column] = acts_as_list_top
486
483
  end
487
484
  end
485
+
486
+ # When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)
487
+ def quoted_position_column
488
+ @_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
489
+ end
490
+
491
+ # Used in order clauses
492
+ def quoted_table_name
493
+ @_quoted_table_name ||= acts_as_list_class.quoted_table_name
494
+ end
488
495
  end
489
496
  end
490
497
  end
@@ -1,7 +1,7 @@
1
1
  module ActiveRecord
2
2
  module Acts
3
3
  module List
4
- VERSION = '0.7.2'
4
+ VERSION = '0.7.5'
5
5
  end
6
6
  end
7
7
  end
data/test/helper.rb CHANGED
@@ -11,4 +11,11 @@ require "active_record"
11
11
  require "minitest/autorun"
12
12
  require "#{File.dirname(__FILE__)}/../init"
13
13
 
14
+ if defined?(ActiveRecord::VERSION) &&
15
+ ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2
16
+
17
+ # Was removed in Rails 5 and is effectively true.
18
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
19
+ end
20
+
14
21
  require "shared"
data/test/shared.rb CHANGED
@@ -6,4 +6,5 @@ module Shared
6
6
  autoload :ArrayScopeList, 'shared_array_scope_list'
7
7
  autoload :TopAddition, 'shared_top_addition'
8
8
  autoload :NoAddition, 'shared_no_addition'
9
+ autoload :Quoting, 'shared_quoting'
9
10
  end
data/test/shared_list.rb CHANGED
@@ -142,7 +142,7 @@ module Shared
142
142
 
143
143
  def test_update_position_when_scope_changes
144
144
  assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
145
- parent = ListMixin.create(parent_id: 6)
145
+ ListMixin.create(parent_id: 6)
146
146
 
147
147
  ListMixin.where(id: 2).first.move_within_scope(6)
148
148
 
@@ -246,5 +246,11 @@ module Shared
246
246
 
247
247
  assert_equal [5, 1, 6, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
248
248
  end
249
+
250
+ def test_non_persisted_records_dont_get_lock_called
251
+ new = ListMixin.new(parent_id: 5)
252
+
253
+ new.destroy
254
+ end
249
255
  end
250
256
  end
@@ -0,0 +1,21 @@
1
+ module Shared
2
+ module Quoting
3
+
4
+ def setup
5
+ 3.times { |counter| QuotedList.create! order: counter }
6
+ end
7
+
8
+ def test_create
9
+ assert_equal QuotedList.in_list.size, 3
10
+ end
11
+
12
+ # This test execute raw queries involving table name
13
+ def test_moving
14
+ item = QuotedList.first
15
+ item.higher_items
16
+ item.lower_items
17
+ item.send :bottom_item # Part of private api
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ require 'helper'
2
+
3
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
4
+ ActiveRecord::Schema.verbose = false
5
+
6
+ class Section < ActiveRecord::Base
7
+ has_many :items
8
+ acts_as_list
9
+
10
+ scope :visible, -> { where(visible: true) }
11
+ end
12
+
13
+ class Item < ActiveRecord::Base
14
+ belongs_to :section
15
+ acts_as_list scope: :section
16
+
17
+ scope :visible, -> { where(visible: true).joins(:section).merge(Section.visible) }
18
+ end
19
+
20
+ class JoinedTestCase < Minitest::Test
21
+ def setup
22
+ ActiveRecord::Base.connection.create_table :sections do |t|
23
+ t.column :position, :integer
24
+ t.column :visible, :boolean, default: true
25
+ end
26
+
27
+ ActiveRecord::Base.connection.create_table :items do |t|
28
+ t.column :position, :integer
29
+ t.column :section_id, :integer
30
+ t.column :visible, :boolean, default: true
31
+ end
32
+
33
+ ActiveRecord::Base.connection.schema_cache.clear!
34
+ [Section, Item].each(&:reset_column_information)
35
+ super
36
+ end
37
+
38
+ def teardown
39
+ ActiveRecord::Base.connection.tables.each do |table|
40
+ ActiveRecord::Base.connection.drop_table(table)
41
+ end
42
+ super
43
+ end
44
+ end
45
+
46
+ # joining the relation returned by `#higher_items` or `#lower_items` to another table
47
+ # previously could result in ambiguous column names in the query
48
+ class TestHigherLowerItems < JoinedTestCase
49
+ def test_higher_items
50
+ section = Section.create
51
+ item1 = Item.create section: section
52
+ item2 = Item.create section: section
53
+ item3 = Item.create section: section
54
+ assert_equal item3.higher_items.visible, [item1, item2]
55
+ end
56
+
57
+ def test_lower_items
58
+ section = Section.create
59
+ item1 = Item.create section: section
60
+ item2 = Item.create section: section
61
+ item3 = Item.create section: section
62
+ assert_equal item1.lower_items.visible, [item2, item3]
63
+ end
64
+ end
data/test/test_list.rb CHANGED
@@ -16,9 +16,14 @@ def setup_db(position_options = {})
16
16
  t.column :state, :integer
17
17
  end
18
18
 
19
+ # This table is used to test table names and column names quoting
20
+ ActiveRecord::Base.connection.create_table 'table-name' do |t|
21
+ t.column :order, :integer
22
+ end
23
+
19
24
  mixins = [ Mixin, ListMixin, ListMixinSub1, ListMixinSub2, ListWithStringScopeMixin,
20
25
  ArrayScopeListMixin, ZeroBasedMixin, DefaultScopedMixin,
21
- DefaultScopedWhereMixin, TopAdditionMixin, NoAdditionMixin ]
26
+ DefaultScopedWhereMixin, TopAdditionMixin, NoAdditionMixin, QuotedList ]
22
27
 
23
28
  mixins << EnumArrayScopeListMixin if rails_4
24
29
 
@@ -42,7 +47,13 @@ def rails_4
42
47
  end
43
48
 
44
49
  def teardown_db
45
- ActiveRecord::Base.connection.tables.each do |table|
50
+ if ActiveRecord::VERSION::MAJOR >= 5
51
+ tables = ActiveRecord::Base.connection.data_sources
52
+ else
53
+ tables = ActiveRecord::Base.connection.tables
54
+ end
55
+
56
+ tables.each do |table|
46
57
  ActiveRecord::Base.connection.drop_table(table)
47
58
  end
48
59
  end
@@ -109,6 +120,29 @@ class NoAdditionMixin < Mixin
109
120
  acts_as_list column: "pos", add_new_at: nil, scope: :parent_id
110
121
  end
111
122
 
123
+ ##
124
+ # The way we track changes to
125
+ # scope and position can get tripped up
126
+ # by someone using update_attributes within
127
+ # a callback because it causes multiple passes
128
+ # through the callback chain
129
+ module CallbackMixin
130
+
131
+ def self.included(base)
132
+ base.send :include, InstanceMethods
133
+ base.after_create :change_field
134
+ end
135
+
136
+ module InstanceMethods
137
+ def change_field
138
+ # doesn't matter what column changes, just
139
+ # need to change something
140
+
141
+ self.update_attributes(active: !self.active)
142
+ end
143
+ end
144
+ end
145
+
112
146
  class TheAbstractClass < ActiveRecord::Base
113
147
  self.abstract_class = true
114
148
  self.table_name = 'mixins'
@@ -126,6 +160,11 @@ end
126
160
  class TheBaseSubclass < TheBaseClass
127
161
  end
128
162
 
163
+ class QuotedList < ActiveRecord::Base
164
+ self.table_name = 'table-name'
165
+ acts_as_list column: :order
166
+ end
167
+
129
168
  class ActsAsListTestCase < Minitest::Test
130
169
  # No default test required as this class is abstract.
131
170
  # Need for test/unit.
@@ -163,6 +202,18 @@ class ListTest < ActsAsListTestCase
163
202
  end
164
203
  end
165
204
 
205
+ class ListWithCallbackTest < ActsAsListTestCase
206
+
207
+ include Shared::List
208
+
209
+ def setup
210
+ ListMixin.send(:include, CallbackMixin)
211
+ setup_db
212
+ super
213
+ end
214
+
215
+ end
216
+
166
217
  class ListTestWithDefault < ActsAsListTestCase
167
218
  include Shared::List
168
219
 
@@ -208,6 +259,15 @@ class ArrayScopeListTestWithDefault < ActsAsListTestCase
208
259
  end
209
260
  end
210
261
 
262
+ class QuotingTestList < ActsAsListTestCase
263
+ include Shared::Quoting
264
+
265
+ def setup
266
+ setup_db_with_default
267
+ super
268
+ end
269
+ end
270
+
211
271
  class DefaultScopedTest < ActsAsListTestCase
212
272
  def setup
213
273
  setup_db
@@ -568,3 +628,66 @@ class MultipleListsArrayScopeTest < ActsAsListTestCase
568
628
  assert_equal [1], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order(:pos).map(&:pos)
569
629
  end
570
630
  end
631
+
632
+ class TouchTest < ActsAsListTestCase
633
+ def setup
634
+ setup_db
635
+ 4.times { ListMixin.create! updated_at: yesterday }
636
+ end
637
+
638
+ def now
639
+ Time.now.utc
640
+ end
641
+
642
+ def yesterday
643
+ 1.day.ago
644
+ end
645
+
646
+ def updated_ats
647
+ ListMixin.pluck(:updated_at)
648
+ end
649
+
650
+ def test_moving_item_lower_touches_self_and_lower_item
651
+ ListMixin.first.move_lower
652
+ updated_ats[0..1].each do |updated_at|
653
+ assert_in_delta updated_at, now, 1.second
654
+ end
655
+ updated_ats[2..3].each do |updated_at|
656
+ assert_in_delta updated_at, yesterday, 1.second
657
+ end
658
+ end
659
+
660
+ def test_moving_item_higher_touches_self_and_higher_item
661
+ ListMixin.all.second.move_higher
662
+ updated_ats[0..1].each do |updated_at|
663
+ assert_in_delta updated_at, now, 1.second
664
+ end
665
+ updated_ats[2..3].each do |updated_at|
666
+ assert_in_delta updated_at, yesterday, 1.second
667
+ end
668
+ end
669
+
670
+ def test_moving_item_to_bottom_touches_all_other_items
671
+ ListMixin.first.move_to_bottom
672
+ updated_ats.each do |updated_at|
673
+ assert_in_delta updated_at, now, 1.second
674
+ end
675
+ end
676
+
677
+ def test_moving_item_to_top_touches_all_other_items
678
+ ListMixin.last.move_to_top
679
+ updated_ats.each do |updated_at|
680
+ assert_in_delta updated_at, now, 1.second
681
+ end
682
+ end
683
+
684
+ def test_removing_item_touches_all_lower_items
685
+ ListMixin.all.third.remove_from_list
686
+ updated_ats[0..1].each do |updated_at|
687
+ assert_in_delta updated_at, yesterday, 1.second
688
+ end
689
+ updated_ats[2..2].each do |updated_at|
690
+ assert_in_delta updated_at, now, 1.second
691
+ end
692
+ end
693
+ end