Skip to content

Commit

Permalink
Add acts_as_list_no_update for disabling callbacks (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
randoum authored and brendon committed Jan 18, 2017
1 parent b5a54aa commit 7e3b00a
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 14 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ default: `1`. Use this option to define the top of the list. Use 0 to make the c
- `add_new_at`
default: `:bottom`. Use this option to specify whether objects get added to the `:top` or `:bottom` of the list. `nil` will result in new items not being added to the list on create, i.e, position will be kept nil after create.

## Disabling temporarily

If you need to temporarily disable `acts_as_list` during specific operations such as mass-update or imports:
```ruby
TodoItem.acts_as_list_no_update do
perform_mass_update
end
```
In an `acts_as_list_no_update` block, all callbacks are disabled, and positions are not updated. New records will be created with
the default value from the database. It is your responsibility to correctly manage `positions` values.

## Versions
As of version `0.7.5` Rails 5 is supported.

Expand Down
3 changes: 2 additions & 1 deletion lib/acts_as_list.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
require "acts_as_list/active_record/acts/list"
require 'acts_as_list/active_record/acts/list'
require "acts_as_list/active_record/acts/column_method_definer"
require "acts_as_list/active_record/acts/scope_method_definer"
require "acts_as_list/active_record/acts/top_of_list_method_definer"
require "acts_as_list/active_record/acts/add_new_at_method_definer"
require "acts_as_list/active_record/acts/aux_method_definer"
require "acts_as_list/active_record/acts/callback_definer"
require 'acts_as_list/active_record/acts/no_update'
10 changes: 5 additions & 5 deletions lib/acts_as_list/active_record/acts/callback_definer.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
module ActiveRecord::Acts::List::CallbackDefiner #:nodoc:
def self.call(caller_class, add_new_at)
caller_class.class_eval do
before_validation :check_top_position
before_validation :check_top_position, unless: :act_as_list_no_update?

before_destroy :lock!
after_destroy :decrement_positions_on_lower_items
after_destroy :decrement_positions_on_lower_items, unless: :act_as_list_no_update?

before_update :check_scope
after_update :update_positions
before_update :check_scope, unless: :act_as_list_no_update?
after_update :update_positions, unless: :act_as_list_no_update?

after_commit :clear_scope_changed

if add_new_at.present?
before_create "add_to_list_#{add_new_at}".to_sym
before_create "add_to_list_#{add_new_at}".to_sym, unless: :act_as_list_no_update?
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/acts_as_list/active_record/acts/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def acts_as_list(options = {})
ActiveRecord::Acts::List::CallbackDefiner.call(caller_class, configuration[:add_new_at])

include ActiveRecord::Acts::List::InstanceMethods
include ActiveRecord::Acts::List::NoUpdate
end
end

Expand Down
50 changes: 50 additions & 0 deletions lib/acts_as_list/active_record/acts/no_update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module ActiveRecord
module Acts
module List
module NoUpdate
extend ActiveSupport::Concern

module ClassMethods
# Lets you selectively disable all act_as_list database updates
# for the duration of a block.
#
# ==== Examples
# ActiveRecord::Acts::List.acts_as_list_no_update do
# TodoList....
# end
#
# TodoList.acts_as_list_no_update do
# TodoList....
# end
#
def acts_as_list_no_update(&block)
NoUpdate.apply_to(self, &block)
end
end

class << self
def apply_to(klass)
klasses.push(klass)
yield
ensure
klasses.pop
end

def applied_to?(klass)
klasses.any? { |k| k >= klass }
end

private

def klasses
Thread.current[:act_as_list_no_update] ||= []
end
end

def act_as_list_no_update?
NoUpdate.applied_to?(self.class)
end
end
end
end
end
17 changes: 17 additions & 0 deletions test/shared_array_scope_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ def test_insert
assert !new.first?
assert new.last?

new = ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass') }
assert_equal $default_position,new.pos
assert_equal $default_position.is_a?(Fixnum), new.first?
assert !new.last?

new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 3, new.pos
assert !new.first?
Expand All @@ -81,6 +86,9 @@ def test_insert_at
new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 3, new.pos

new_noup = ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass') }
assert_equal $default_position,new_noup.pos

new4 = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 4, new4.pos

Expand All @@ -104,6 +112,9 @@ def test_insert_at

new4.reload
assert_equal 5, new4.pos

new_noup.reload
assert_equal $default_position, new_noup.pos
end

def test_delete_middle
Expand All @@ -123,6 +134,12 @@ def test_delete_middle

assert_equal 1, ArrayScopeListMixin.where(id: 3).first.pos
assert_equal 2, ArrayScopeListMixin.where(id: 4).first.pos

ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.where(id: 3).first.destroy }

assert_equal [4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)

assert_equal 2, ArrayScopeListMixin.where(id: 4).first.pos
end

def test_remove_from_list_should_then_fail_in_list?
Expand Down
40 changes: 33 additions & 7 deletions test/shared_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ def test_insert
assert !new.first?
assert new.last?

new = ListMixin.acts_as_list_no_update { ListMixin.create(parent_id: 20) }
assert_equal $default_position, new.pos
assert_equal $default_position.is_a?(Fixnum), new.first?
assert !new.last?

new = ListMixin.create(parent_id: 20)
assert_equal 3, new.pos
assert !new.first?
Expand All @@ -81,6 +86,9 @@ def test_insert_at
new = ListMixin.create(parent_id: 20)
assert_equal 3, new.pos

new_noup = ListMixin.acts_as_list_no_update { ListMixin.create(parent_id: 20) }
assert_equal $default_position, new_noup.pos

new4 = ListMixin.create(parent_id: 20)
assert_equal 4, new4.pos

Expand All @@ -105,18 +113,20 @@ def test_insert_at
new4.reload
assert_equal 5, new4.pos

last1 = ListMixin.order('pos').last
last2 = ListMixin.order('pos').last
new_noup.reload
assert_equal $default_position, new_noup.pos

last1 = ListMixin.where('pos IS NOT NULL').order('pos').last
last2 = ListMixin.where('pos IS NOT NULL').order('pos').last
last1.insert_at(1)
last2.insert_at(1)
assert_equal [1, 2, 3, 4, 5], ListMixin.where(parent_id: 20).order('pos').map(&:pos)
pos_list = ListMixin.where(parent_id: 20).order("pos ASC#{' NULLS FIRST' if ENV['DB'] == 'postgresql'}").map(&:pos)
assert_equal [$default_position, 1, 2, 3, 4, 5], pos_list
end

def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)



ListMixin.where(id: 2).first.destroy

assert_equal [1, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
Expand All @@ -131,6 +141,12 @@ def test_delete_middle

assert_equal 1, ListMixin.where(id: 3).first.pos
assert_equal 2, ListMixin.where(id: 4).first.pos

ListMixin.acts_as_list_no_update { ListMixin.where(id: 3).first.destroy }

assert_equal [4], ListMixin.where(parent_id: 5).order('pos').map(&:id)

assert_equal 2, ListMixin.where(id: 4).first.pos
end

def test_with_string_based_scope
Expand Down Expand Up @@ -241,14 +257,24 @@ def test_before_create_callback_adds_to_given_position

assert_equal [5, 1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)

new6 = ListMixin.new(parent_id: 5)
new6.pos = 3
new6.save!
assert_equal 3, new6.pos
assert !new6.first?
assert !new6.last?

assert_equal [5, 1, 6, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)

new = ListMixin.new(parent_id: 5)
new.pos = 3
new.save!
ListMixin.acts_as_list_no_update { new.save! }
assert_equal 3, new.pos
assert_equal 3, new6.pos
assert !new.first?
assert !new.last?

assert_equal [5, 1, 6, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
assert_equal [5, 1, 6, 7, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos, id').map(&:id)
end

def test_non_persisted_records_dont_get_lock_called
Expand Down
12 changes: 12 additions & 0 deletions test/shared_list_sub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def test_insert_at
new = ListMixinSub1.create("parent_id" => 20)
assert_equal 3, new.pos

new_noup = ListMixinSub1.acts_as_list_no_update { ListMixinSub1.create("parent_id" => 20) }
assert_equal $default_position, new_noup.pos

new4 = ListMixin.create("parent_id" => 20)
assert_equal 4, new4.pos

Expand All @@ -145,6 +148,9 @@ def test_insert_at

new4.reload
assert_equal 5, new4.pos

new_noup.reload
assert_equal $default_position, new_noup.pos
end

def test_delete_middle
Expand All @@ -164,6 +170,12 @@ def test_delete_middle

assert_equal 1, ListMixin.where(id: 3).first.pos
assert_equal 2, ListMixin.where(id: 4).first.pos

ListMixin.acts_as_list_no_update { ListMixin.where(id: 3).first.destroy }

assert_equal [4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)

assert_equal 2, ListMixin.where(id: 4).first.pos
end

def test_acts_as_list_class
Expand Down
17 changes: 16 additions & 1 deletion test/shared_top_addition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ def test_insert
assert new.first?
assert !new.last?

new = TopAdditionMixin.acts_as_list_no_update { TopAdditionMixin.create(parent_id: 20) }
assert_equal $default_position, new.pos
assert_equal $default_position.is_a?(Fixnum), new.first?
assert !new.last?

new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos
assert new.first?
assert_equal $default_position.nil?, new.first?
assert !new.last?

new = TopAdditionMixin.create(parent_id: 0)
Expand All @@ -67,6 +72,9 @@ def test_insert_at
new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos

new = TopAdditionMixin.acts_as_list_no_update { TopAdditionMixin.create(parent_id: 20) }
assert_equal $default_position, new.pos

new4 = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new4.pos

Expand All @@ -87,6 +95,13 @@ def test_delete_middle
assert_equal 1, TopAdditionMixin.where(id: 1).first.pos
assert_equal 2, TopAdditionMixin.where(id: 3).first.pos
assert_equal 3, TopAdditionMixin.where(id: 4).first.pos

TopAdditionMixin.acts_as_list_no_update { TopAdditionMixin.where(id: 3).first.destroy }

assert_equal [1, 4], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)

assert_equal 1, TopAdditionMixin.where(id: 1).first.pos
assert_equal 3, TopAdditionMixin.where(id: 4).first.pos
end

end
Expand Down
11 changes: 11 additions & 0 deletions test/shared_zero_based.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ def test_insert
assert !new.first?
assert new.last?

new = ZeroBasedMixin.acts_as_list_no_update { ZeroBasedMixin.create(parent_id: 20) }
assert_equal $default_position, new.pos
assert !new.first?
assert !new.last?

new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 2, new.pos
assert !new.first?
Expand Down Expand Up @@ -63,6 +68,9 @@ def test_insert_at
new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 2, new.pos

new_noup = ZeroBasedMixin.acts_as_list_no_update { ZeroBasedMixin.create(parent_id: 20) }
assert_equal $default_position, new_noup.pos

new4 = ZeroBasedMixin.create(parent_id: 20)
assert_equal 3, new4.pos

Expand All @@ -86,6 +94,9 @@ def test_insert_at

new4.reload
assert_equal 4, new4.pos

new_noup.reload
assert_equal $default_position, new_noup.pos
end
end
end
Loading

0 comments on commit 7e3b00a

Please sign in to comment.