acts_as_list-1.0.4/ 0000755 0000041 0000041 00000000000 14042233640 014212 5 ustar www-data www-data acts_as_list-1.0.4/.travis.yml 0000644 0000041 0000041 00000003130 14042233640 016320 0 ustar www-data www-data language: ruby
cache: bundler
sudo: false
before_script:
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
- gem install bundler -v '< 2'
- mysql -e 'create database acts_as_list;'
- psql -c 'create database acts_as_list;' -U postgres
rvm:
- 2.4
- 2.5
- 2.6
- 2.7
- 3.0
services:
- mysql
- postgresql
env:
- DB=sqlite
- DB=mysql
- DB=postgresql
gemfile:
- gemfiles/rails_4_2.gemfile
- gemfiles/rails_5_0.gemfile
- gemfiles/rails_5_1.gemfile
- gemfiles/rails_5_2.gemfile
- gemfiles/rails_6_0.gemfile
- gemfiles/rails_6_1.gemfile
matrix:
exclude:
- rvm: 2.4
gemfile: gemfiles/rails_6_0.gemfile
- rvm: 2.4
gemfile: gemfiles/rails_6_1.gemfile
# Ruby 2.7 uses a `bigdecimal` version that doesn't support BigDecimal.new
# that Rails 4.2 uses. See also:
# https://github.com/ruby/bigdecimal#which-version-should-you-select
- rvm: 2.7
gemfile: gemfiles/rails_4_2.gemfile
# Ruby 2.7 warning `sqlite3-1.3.13/lib/sqlite3/statement.rb:108: warning: rb_tainted_str_new is deprecated`
# and job wil exceed the maximum log length
# see also: https://github.com/sparklemotion/sqlite3-ruby/issues/276
- rvm: 2.7
gemfile: gemfiles/rails_5_0.gemfile
env: DB=sqlite
# Rails <6 does not support Ruby 3, see:
# https://github.com/rails/rails/issues/40938#issuecomment-751898275
- rvm: 3.0
gemfile: gemfiles/rails_4_2.gemfile
- rvm: 3.0
gemfile: gemfiles/rails_5_0.gemfile
- rvm: 3.0
gemfile: gemfiles/rails_5_1.gemfile
- rvm: 3.0
gemfile: gemfiles/rails_5_2.gemfile
acts_as_list-1.0.4/test/ 0000755 0000041 0000041 00000000000 14042233640 015171 5 ustar www-data www-data acts_as_list-1.0.4/test/shared_top_addition.rb 0000644 0000041 0000041 00000007156 14042233640 021532 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module TopAddition
def setup
(1..4).each { |counter| TopAdditionMixin.create! pos: counter, parent_id: 5 }
end
def test_setup_state
# If we explicitly define a position (as above) then that position is what gets applied
assert_equal [1, 2, 3, 4], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_reordering
TopAdditionMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
TopAdditionMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
TopAdditionMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
TopAdditionMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
TopAdditionMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
TopAdditionMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_injection
item = TopAdditionMixin.new(parent_id: 1)
assert_equal({ parent_id: 1 }, item.scope_condition)
assert_equal "pos", item.position_column
end
def test_insert
new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos
assert new.first?
assert new.last?
new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos
assert new.first?
assert !new.last?
new = TopAdditionMixin.acts_as_list_no_update { TopAdditionMixin.create(parent_id: 20) }
assert_equal_or_nil $default_position, new.pos
assert_equal $default_position.is_a?(Integer), new.first?
assert !new.last?
new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos
assert_equal $default_position.nil?, new.first?
assert !new.last?
new = TopAdditionMixin.create(parent_id: 0)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_insert_at
new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos
new = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new.pos
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_or_nil $default_position, new.pos
new4 = TopAdditionMixin.create(parent_id: 20)
assert_equal 1, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
end
def test_supplied_position
new = TopAdditionMixin.create(parent_id: 20, pos: 3)
assert_equal 3, new.pos
end
def test_delete_middle
TopAdditionMixin.where(id: 2).first.destroy
assert_equal [1, 3, 4], TopAdditionMixin.where(parent_id: 5).order('pos').map(&:id)
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
end
acts_as_list-1.0.4/test/helper.rb 0000644 0000041 0000041 00000003400 14042233640 016772 0 ustar www-data www-data # frozen_string_literal: true
# $DEBUG = true
require "rubygems"
require "bundler/setup"
begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end
require "active_record"
require "minitest/autorun"
require "mocha/minitest"
require "#{File.dirname(__FILE__)}/../init"
if defined?(ActiveRecord::VERSION) &&
ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2
# Was removed in Rails 5 and is effectively true.
ActiveRecord::Base.raise_in_transactional_callbacks = true
end
db_config = YAML.load_file(File.expand_path("../database.yml", __FILE__)).fetch(ENV["DB"] || "sqlite")
ActiveRecord::Base.establish_connection(db_config)
ActiveRecord::Schema.verbose = false
def teardown_db
if ActiveRecord::VERSION::MAJOR >= 5
tables = ActiveRecord::Base.connection.data_sources
else
tables = ActiveRecord::Base.connection.tables
end
tables.each do |table|
ActiveRecord::Base.connection.drop_table(table)
end
end
require "shared"
# require 'logger'
# ActiveRecord::Base.logger = Logger.new(STDOUT)
def assert_equal_or_nil(a, b)
if a.nil?
assert_nil b
else
assert_equal a, b
end
end
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')
original_behavior = ActiveSupport::Deprecation.behavior
ActiveSupport::Deprecation.behavior = :raise
begin
yield
rescue ActiveSupport::DeprecationException => e
flunk "#{failure_message}: #{e}"
rescue
raise
else
pass pass_message
end
ensure
ActiveSupport::Deprecation.behavior = original_behavior
end
acts_as_list-1.0.4/test/test_no_update_for_extra_classes.rb 0000644 0000041 0000041 00000007310 14042233640 024322 0 ustar www-data www-data # frozen_string_literal: true
require 'helper'
class TodoList < ActiveRecord::Base
has_many :todo_items
acts_as_list
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
has_many :todo_item_attachments
acts_as_list scope: :todo_list
end
class TodoItemAttachment < ActiveRecord::Base
belongs_to :todo_item
acts_as_list scope: :todo_item
end
class NoUpdateForCollectionClassesTestCase < Minitest::Test
def setup
ActiveRecord::Base.connection.create_table :todo_lists do |t|
t.column :position, :integer
end
ActiveRecord::Base.connection.create_table :todo_items do |t|
t.column :position, :integer
t.column :todo_list_id, :integer
end
ActiveRecord::Base.connection.create_table :todo_item_attachments do |t|
t.column :position, :integer
t.column :todo_item_id, :integer
end
ActiveRecord::Base.connection.schema_cache.clear!
[TodoList, TodoItem, TodoItemAttachment].each(&:reset_column_information)
super
end
def teardown
teardown_db
super
end
end
class NoUpdateForCollectionClassesTest < NoUpdateForCollectionClassesTestCase
def setup
super
@list_1, @list_2 = (1..2).map { |counter| TodoList.create!(position: counter) }
@item_1, @item_2 = (1..2).map { |counter| TodoItem.create!(position: counter, todo_list_id: @list_1.id) }
@attachment_1, @attachment_2 = (1..2).map { |counter| TodoItemAttachment.create!(position: counter, todo_item_id: @item_1.id) }
@attachment_3, @attachment_4 = (1..2).map {|counter| TodoItemAttachment.create!(position: counter, todo_item_id: @item_2.id)}
end
def test_update
@item_1.update position: 2
assert_equal 2, @item_1.reload.position
assert_equal 1, @item_2.reload.position
end
def test_no_update_for_single_class_instances
TodoItem.acts_as_list_no_update { @item_1.update position: 2 }
assert_equal 2, @item_1.reload.position
assert_equal 2, @item_2.reload.position
end
def test_no_update_for_different_class_instances
TodoItem.acts_as_list_no_update([TodoItemAttachment]) { update_records! }
assert_equal 2, @item_1.reload.position
assert_equal 2, @item_2.reload.position
assert_equal 2, @attachment_1.reload.position
assert_equal 2, @attachment_2.reload.position
assert_equal 2, @list_1.reload.position
assert_equal 1, @list_2.reload.position
end
def test_no_update_for_nested_blocks
new_list = @list_1.dup
new_list.save!
TodoItem.acts_as_list_no_update do
@list_1.todo_items.reverse.each do |item|
new_item = item.dup
new_list.todo_items << new_item
new_item.save!
assert_equal new_item.position, item.reload.position
TodoItemAttachment.acts_as_list_no_update do
item.todo_item_attachments.reverse.each do |attach|
new_attach = attach.dup
new_item.todo_item_attachments << new_attach
new_attach.save!
assert_equal new_attach.position, attach.reload.position
end
end
end
end
end
def test_raising_array_type_error
exception = assert_raises ActiveRecord::Acts::List::NoUpdate::ArrayTypeError do
TodoList.acts_as_list_no_update(nil)
end
assert_equal("The first argument must be an array", exception.message )
end
def test_non_disparity_classes_error
exception = assert_raises ActiveRecord::Acts::List::NoUpdate::DisparityClassesError do
TodoList.acts_as_list_no_update([Class])
end
assert_equal("The first argument should contain ActiveRecord or ApplicationRecord classes", exception.message )
end
private
def update_records!
@item_1.update position: 2
@attachment_1.update position: 2
@list_1.update position: 2
end
end
acts_as_list-1.0.4/test/shared_list_sub.rb 0000644 0000041 0000041 00000014557 14042233640 020704 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module ListSub
def setup
(1..4).each do |i|
node = ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).new parent_id: 5000
node.pos = i
node.save!
end
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 3).first.move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.where(id: 2).first, ListMixin.where(id: 1).first.lower_item
assert_nil ListMixin.where(id: 1).first.higher_item
assert_equal ListMixin.where(id: 3).first, ListMixin.where(id: 4).first.higher_item
assert_nil ListMixin.where(id: 4).first.lower_item
end
def test_next_prev_not_regular_sequence
ListMixin.all.each do |item|
item.update pos: item.pos * 5
end
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
assert_equal [5, 10, 15, 20], ListMixin.where(parent_id: 5000).order('id').map(&:pos)
ListMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
assert_equal [5, 15, 10, 20], ListMixin.where(parent_id: 5000).order('id').map(&:pos)
ListMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
assert_equal [5, 10, 15, 20], ListMixin.where(parent_id: 5000).order('id').map(&:pos)
ListMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
end
def test_next_prev_groups
li1 = ListMixin.where(id: 1).first
li2 = ListMixin.where(id: 2).first
li3 = ListMixin.where(id: 3).first
li4 = ListMixin.where(id: 4).first
assert_equal [li2, li3, li4], li1.lower_items
assert_equal [li4], li3.lower_items
assert_equal [li2, li3], li1.lower_items(2)
assert_equal [], li4.lower_items
assert_equal [li2, li1], li3.higher_items
assert_equal [li1], li2.higher_items
assert_equal [li3, li2], li4.higher_items(2)
assert_equal [], li1.higher_items
end
def test_next_prev_groups_with_same_position
li1 = ListMixin.where(id: 1).first
li2 = ListMixin.where(id: 2).first
li3 = ListMixin.where(id: 3).first
li4 = ListMixin.where(id: 4).first
li3.update_column(:pos, 2) # Make the same position as li2
assert_equal [1, 2, 2, 4], ListMixin.order(:pos).pluck(:pos)
assert_equal [li3, li4], li2.lower_items
assert_equal [li2, li4], li3.lower_items
assert_equal [li3, li1], li2.higher_items
assert_equal [li2, li1], li3.higher_items
end
def test_injection
item = ListMixin.new("parent_id"=>1)
assert_equal({ parent_id: 1 }, item.scope_condition)
assert_equal "pos", item.position_column
end
def test_insert_at
new = ListMixin.create("parent_id" => 20)
assert_equal 1, new.pos
new = ListMixinSub1.create("parent_id" => 20)
assert_equal 2, new.pos
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_or_nil $default_position, new_noup.pos
new4 = ListMixin.create("parent_id" => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixinSub1.create("parent_id" => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
new_noup.reload
assert_equal_or_nil $default_position, new_noup.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
ListMixin.where(id: 2).first.destroy
assert_equal [1, 3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
assert_equal 1, ListMixin.where(id: 1).first.pos
assert_equal 2, ListMixin.where(id: 3).first.pos
assert_equal 3, ListMixin.where(id: 4).first.pos
ListMixin.where(id: 1).first.destroy
assert_equal [3, 4], ListMixin.where(parent_id: 5000).order('pos').map(&:id)
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
assert_equal TheBaseClass, TheBaseSubclass.new.acts_as_list_class
assert_equal TheAbstractSubclass, TheAbstractSubclass.new.acts_as_list_class
end
end
end
acts_as_list-1.0.4/test/shared_quoting.rb 0000644 0000041 0000041 00000000677 14042233640 020544 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module Quoting
def setup
3.times { |counter| QuotedList.create! order: counter }
end
def test_create
assert_equal QuotedList.in_list.size, 3
end
# This test execute raw queries involving table name
def test_moving
item = QuotedList.first
item.higher_items
item.lower_items
item.send :bottom_item # Part of private api
end
end
end
acts_as_list-1.0.4/test/shared_array_scope_list.rb 0000644 0000041 0000041 00000016471 14042233640 022417 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module ArrayScopeList
def setup
(1..4).each { |counter| ArrayScopeListMixin.create! pos: counter, parent_id: 5, parent_type: 'ParentClass' }
(1..4).each { |counter| ArrayScopeListMixin.create! pos: counter, parent_id: 6, parent_type: 'ParentClass' }
end
def test_reordering
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 4).first.insert_at(4)
assert_equal [1, 3, 2, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:pos)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 3).first.move_to_bottom
assert_equal [1, 2, 4, 3], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
end
def test_next_prev
assert_equal ArrayScopeListMixin.where(id: 2).first, ArrayScopeListMixin.where(id: 1).first.lower_item
assert_nil ArrayScopeListMixin.where(id: 1).first.higher_item
assert_equal ArrayScopeListMixin.where(id: 3).first, ArrayScopeListMixin.where(id: 4).first.higher_item
assert_nil ArrayScopeListMixin.where(id: 4).first.lower_item
end
def test_injection
item = ArrayScopeListMixin.new(parent_id: 1, parent_type: 'ParentClass')
assert_equal "pos", item.position_column
end
def test_insert
new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 1, new.pos
assert new.first?
assert new.last?
new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 2, new.pos
assert !new.first?
assert new.last?
new = ArrayScopeListMixin.acts_as_list_no_update { ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass') }
assert_equal_or_nil $default_position,new.pos
assert_equal $default_position.is_a?(Integer), new.first?
assert !new.last?
new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 3, new.pos
assert !new.first?
assert new.last?
new = ArrayScopeListMixin.create(parent_id: 0, parent_type: 'ParentClass')
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_insert_at
new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 1, new.pos
new = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 2, new.pos
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_or_nil $default_position,new_noup.pos
new4 = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ArrayScopeListMixin.create(parent_id: 20, parent_type: 'ParentClass')
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
new_noup.reload
assert_equal_or_nil $default_position, new_noup.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 2).first.destroy
assert_equal [1, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
assert_equal 1, ArrayScopeListMixin.where(id: 1).first.pos
assert_equal 2, ArrayScopeListMixin.where(id: 3).first.pos
assert_equal 3, ArrayScopeListMixin.where(id: 4).first.pos
ArrayScopeListMixin.where(id: 1).first.destroy
assert_equal [3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
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?
assert_equal true, ArrayScopeListMixin.where(id: 1).first.in_list?
ArrayScopeListMixin.where(id: 1).first.remove_from_list
assert_equal false, ArrayScopeListMixin.where(id: 1).first.in_list?
end
def test_remove_from_list_should_set_position_to_nil
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 2).first.remove_from_list
assert_equal 1, ArrayScopeListMixin.where(id: 1).first.pos
assert_nil ArrayScopeListMixin.where(id: 2).first.pos
assert_equal 2, ArrayScopeListMixin.where(id: 3).first.pos
assert_equal 3, ArrayScopeListMixin.where(id: 4).first.pos
end
def test_remove_before_destroy_does_not_shift_lower_items_twice
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
ArrayScopeListMixin.where(id: 2).first.remove_from_list
ArrayScopeListMixin.where(id: 2).first.destroy
assert_equal [1, 3, 4], ArrayScopeListMixin.where(parent_id: 5, parent_type: 'ParentClass').order('pos').map(&:id)
assert_equal 1, ArrayScopeListMixin.where(id: 1).first.pos
assert_equal 2, ArrayScopeListMixin.where(id: 3).first.pos
assert_equal 3, ArrayScopeListMixin.where(id: 4).first.pos
end
end
end
acts_as_list-1.0.4/test/shared_zero_based.rb 0000644 0000041 0000041 00000005656 14042233640 021175 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module ZeroBased
def setup
(1..4).each { |counter| ZeroBasedMixin.create! pos: counter, parent_id: 5 }
end
def test_insert
new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 0, new.pos
assert new.first?
assert new.last?
new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 1, new.pos
assert !new.first?
assert new.last?
new = ZeroBasedMixin.acts_as_list_no_update { ZeroBasedMixin.create(parent_id: 20) }
assert_equal_or_nil $default_position, new.pos
assert !new.first?
assert !new.last?
new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 2, new.pos
assert !new.first?
assert new.last?
new = ZeroBasedMixin.create(parent_id: 0)
assert_equal 0, new.pos
assert new.first?
assert new.last?
new = ZeroBasedMixin.create(parent_id: 1, pos: -500)
assert_equal 0, new.pos
assert new.first?
assert new.last?
end
def test_reordering
assert_equal [1, 2, 3, 4], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], ZeroBasedMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_insert_at
new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 0, new.pos
new = ZeroBasedMixin.create(parent_id: 20)
assert_equal 1, new.pos
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_or_nil $default_position, new_noup.pos
new4 = ZeroBasedMixin.create(parent_id: 20)
assert_equal 3, new4.pos
new4.insert_at(2)
assert_equal 2, new4.pos
new.reload
assert_equal 3, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 3, new4.pos
new5 = ListMixin.create(parent_id: 20)
assert_equal 4, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 4, new4.pos
new_noup.reload
assert_equal_or_nil $default_position, new_noup.pos
end
end
end
acts_as_list-1.0.4/test/test_joined_list.rb 0000644 0000041 0000041 00000003101 14042233640 021053 0 ustar www-data www-data # frozen_string_literal: true
require 'helper'
class Section < ActiveRecord::Base
has_many :items
acts_as_list
scope :visible, -> { where(visible: true) }
end
class Item < ActiveRecord::Base
belongs_to :section
acts_as_list scope: :section
scope :visible, -> { where(visible: true).joins(:section).merge(Section.visible) }
end
class JoinedTestCase < Minitest::Test
def setup
ActiveRecord::Base.connection.create_table :sections do |t|
t.column :position, :integer
t.column :visible, :boolean, default: true
end
ActiveRecord::Base.connection.create_table :items do |t|
t.column :position, :integer
t.column :section_id, :integer
t.column :visible, :boolean, default: true
end
ActiveRecord::Base.connection.schema_cache.clear!
[Section, Item].each(&:reset_column_information)
super
end
def teardown
teardown_db
super
end
end
# joining the relation returned by `#higher_items` or `#lower_items` to another table
# previously could result in ambiguous column names in the query
class TestHigherLowerItems < JoinedTestCase
def test_higher_items
section = Section.create
item1 = Item.create section: section
item2 = Item.create section: section
item3 = Item.create section: section
assert_equal item3.higher_items.visible, [item2, item1]
end
def test_lower_items
section = Section.create
item1 = Item.create section: section
item2 = Item.create section: section
item3 = Item.create section: section
assert_equal item1.lower_items.visible, [item2, item3]
end
end
acts_as_list-1.0.4/test/test_list.rb 0000644 0000041 0000041 00000076153 14042233640 017544 0 ustar www-data www-data # frozen_string_literal: true
# NOTE: following now done in helper.rb (better Readability)
require 'helper'
def setup_db(position_options = {})
$default_position = position_options[:default]
# sqlite cannot drop/rename/alter columns and add constraints after table creation
sqlite = ENV.fetch("DB", "sqlite") == "sqlite"
# AR caches columns options like defaults etc. Clear them!
ActiveRecord::Base.connection.create_table :mixins do |t|
t.column :pos, :integer, **position_options unless position_options[:positive] && sqlite
t.column :active, :boolean, default: true
t.column :parent_id, :integer
t.column :parent_type, :string
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :state, :integer
end
if position_options[:unique] && !(sqlite && position_options[:positive])
ActiveRecord::Base.connection.add_index :mixins, :pos, unique: true
end
if position_options[:positive]
if sqlite
# SQLite cannot add constraint after table creation, also cannot add unique inside ADD COLUMN
ActiveRecord::Base.connection.execute('ALTER TABLE mixins ADD COLUMN pos integer8 NOT NULL CHECK (pos > 0) DEFAULT 1')
ActiveRecord::Base.connection.execute('CREATE UNIQUE INDEX index_mixins_on_pos ON mixins(pos)')
else
ActiveRecord::Base.connection.execute('ALTER TABLE mixins ADD CONSTRAINT pos_check CHECK (pos > 0)')
end
end
# This table is used to test table names and column names quoting
ActiveRecord::Base.connection.create_table 'table-name' do |t|
t.column :order, :integer
end
# This table is used to test table names with different primary_key columns
ActiveRecord::Base.connection.create_table 'altid-table', primary_key: 'altid' do |t|
t.column :pos, :integer
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
ActiveRecord::Base.connection.add_index 'altid-table', :pos, unique: true
mixins = [ Mixin, ListMixin, ListMixinSub1, ListMixinSub2, ListWithStringScopeMixin,
ArrayScopeListMixin, ZeroBasedMixin, DefaultScopedMixin, EnumArrayScopeListMixin,
DefaultScopedWhereMixin, TopAdditionMixin, NoAdditionMixin, QuotedList, TouchDisabledMixin ]
ActiveRecord::Base.connection.schema_cache.clear!
mixins.each do |klass|
klass.reset_column_information
end
end
def setup_db_with_default
setup_db default: 0
end
class Mixin < ActiveRecord::Base
self.table_name = 'mixins'
end
class ListMixin < Mixin
acts_as_list column: "pos", scope: :parent
end
class TouchDisabledMixin < Mixin
acts_as_list column: "pos", touch_on_update: false
end
class ListMixinSub1 < ListMixin
end
class ListMixinSub2 < ListMixin
validates :pos, presence: true
end
class ListMixinError < ListMixin
validates :state, presence: true
end
class ListWithStringScopeMixin < Mixin
acts_as_list column: "pos", scope: 'parent_id = #{parent_id}'
end
class ArrayScopeListMixin < Mixin
acts_as_list column: "pos", scope: [:parent_id, :parent_type]
end
class ArrayScopeListWithHashMixin < Mixin
acts_as_list column: "pos", scope: [:parent_id, state: nil]
end
class EnumArrayScopeListMixin < Mixin
STATE_VALUES = %w(active archived)
enum state: STATE_VALUES
acts_as_list column: "pos", scope: [:parent_id, :state]
end
class ZeroBasedMixin < Mixin
acts_as_list column: "pos", top_of_list: 0, scope: [:parent_id]
end
class DefaultScopedMixin < Mixin
acts_as_list column: "pos"
default_scope { order('pos ASC') }
end
class DefaultScopedWhereMixin < Mixin
acts_as_list column: "pos"
default_scope { order('pos ASC').where(active: true) }
def self.for_active_false_tests
unscope(:where).where(active: false)
end
end
class SequentialUpdatesDefault < Mixin
acts_as_list column: "pos"
end
class SequentialUpdatesAltId < ActiveRecord::Base
self.table_name = "altid-table"
self.primary_key = "altid"
acts_as_list column: "pos"
end
class SequentialUpdatesAltIdTouchDisabled < SequentialUpdatesAltId
acts_as_list column: "pos", touch_on_update: false
end
class SequentialUpdatesFalseMixin < Mixin
acts_as_list column: "pos", sequential_updates: false
end
class TopAdditionMixin < Mixin
acts_as_list column: "pos", add_new_at: :top, scope: :parent_id
end
class NoAdditionMixin < Mixin
acts_as_list column: "pos", add_new_at: nil, scope: :parent_id
end
##
# The way we track changes to
# scope and position can get tripped up
# by someone using update within
# a callback because it causes multiple passes
# through the callback chain
module CallbackMixin
def self.included(base)
base.send :include, InstanceMethods
base.after_create :change_field
end
module InstanceMethods
def change_field
# doesn't matter what column changes, just
# need to change something
self.update active: !self.active
end
end
end
class TheAbstractClass < ActiveRecord::Base
self.abstract_class = true
self.table_name = 'mixins'
end
class TheAbstractSubclass < TheAbstractClass
acts_as_list column: "pos", scope: :parent
end
class TheBaseClass < ActiveRecord::Base
self.table_name = 'mixins'
acts_as_list column: "pos", scope: :parent
end
class TheBaseSubclass < TheBaseClass
end
class QuotedList < ActiveRecord::Base
self.table_name = 'table-name'
acts_as_list column: :order
end
class ActsAsListTestCase < Minitest::Test
# No default test required as this class is abstract.
# Need for test/unit.
undef_method :default_test if method_defined?(:default_test)
def teardown
teardown_db
end
end
class ZeroBasedTest < ActsAsListTestCase
include Shared::ZeroBased
def setup
setup_db
super
end
end
class ZeroBasedTestWithDefault < ActsAsListTestCase
include Shared::ZeroBased
def setup
setup_db_with_default
super
end
end
class ListTest < ActsAsListTestCase
include Shared::List
def setup
setup_db
super
end
def test_insert_race_condition
# the bigger n is the more likely we will have a race condition
n = 1000
(1..n).each do |counter|
node = ListMixin.new parent_id: 1
node.pos = counter
node.save!
end
wait_for_it = true
threads = []
4.times do |i|
threads << Thread.new do
true while wait_for_it
ActiveRecord::Base.connection_pool.with_connection do |c|
n.times do
begin
ListMixin.where(parent_id: 1).order('pos').last.insert_at(1)
rescue Exception
# ignore SQLite3::SQLException due to table locking
end
end
end
end
end
wait_for_it = false
threads.each(&:join)
assert_equal((1..n).to_a, ListMixin.where(parent_id: 1).order('pos').map(&:pos))
end
end
class ListWithCallbackTest < ActsAsListTestCase
include Shared::List
def setup
ListMixin.send(:include, CallbackMixin)
setup_db
super
end
end
class ListTestWithDefault < ActsAsListTestCase
include Shared::List
def setup
setup_db_with_default
super
end
end
class ListSubTest < ActsAsListTestCase
include Shared::ListSub
def setup
setup_db
super
end
end
class ListSubTestWithDefault < ActsAsListTestCase
include Shared::ListSub
def setup
setup_db_with_default
super
end
end
class ArrayScopeListTest < ActsAsListTestCase
include Shared::ArrayScopeList
def setup
setup_db
super
end
end
class ArrayScopeListTestWithDefault < ActsAsListTestCase
include Shared::ArrayScopeList
def setup
setup_db_with_default
super
end
end
class QuotingTestList < ActsAsListTestCase
include Shared::Quoting
def setup
setup_db_with_default
super
end
end
class DefaultScopedTest < ActsAsListTestCase
def setup
setup_db
(1..4).each { |counter| DefaultScopedMixin.create!({pos: counter}) }
end
def test_insert
new = DefaultScopedMixin.create
assert_equal 5, new.pos
assert !new.first?
assert new.last?
new = DefaultScopedMixin.create
assert_equal 6, new.pos
assert !new.first?
assert new.last?
new = DefaultScopedMixin.acts_as_list_no_update { DefaultScopedMixin.create }
assert_equal_or_nil $default_position, new.pos
assert_equal $default_position.is_a?(Integer), new.first?
assert !new.last?
new = DefaultScopedMixin.create
assert_equal 7, new.pos
assert !new.first?
assert new.last?
end
def test_reordering
assert_equal [1, 2, 3, 4], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], DefaultScopedMixin.all.map(&:id)
end
def test_insert_at
new = DefaultScopedMixin.create
assert_equal 5, new.pos
new = DefaultScopedMixin.create
assert_equal 6, new.pos
new_noup = DefaultScopedMixin.acts_as_list_no_update { DefaultScopedMixin.create }
assert_equal_or_nil $default_position, new_noup.pos
new = DefaultScopedMixin.create
assert_equal 7, new.pos
new4 = DefaultScopedMixin.create
assert_equal 8, new4.pos
new4.insert_at(2)
assert_equal 2, new4.pos
new.reload
assert_equal 8, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 3, new4.pos
new5 = DefaultScopedMixin.create
assert_equal 9, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 4, new4.pos
new_noup.reload
assert_equal_or_nil $default_position, new_noup.pos
end
def test_find_or_create_doesnt_raise_deprecation_warning
assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
DefaultScopedMixin.find_or_create_by(pos: 5)
end
end
def test_update_position
assert_equal [1, 2, 3, 4], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 2).first.set_list_position(4)
assert_equal [1, 3, 4, 2], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 2).first.set_list_position(2)
assert_equal [1, 2, 3, 4], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 1).first.set_list_position(4)
assert_equal [2, 3, 4, 1], DefaultScopedMixin.all.map(&:id)
DefaultScopedMixin.where(id: 1).first.set_list_position(1)
assert_equal [1, 2, 3, 4], DefaultScopedMixin.all.map(&:id)
end
end
class DefaultScopedWhereTest < ActsAsListTestCase
def setup
setup_db
(1..4).each { |counter| DefaultScopedWhereMixin.create! pos: counter, active: false }
end
def test_insert
new = DefaultScopedWhereMixin.create
assert_equal 5, new.pos
assert !new.first?
assert new.last?
new = DefaultScopedWhereMixin.create
assert_equal 6, new.pos
assert !new.first?
assert new.last?
new = DefaultScopedWhereMixin.acts_as_list_no_update { DefaultScopedWhereMixin.create }
assert_equal_or_nil $default_position, new.pos
assert_equal $default_position.is_a?(Integer), new.first?
assert !new.last?
new = DefaultScopedWhereMixin.create
assert_equal 7, new.pos
assert !new.first?
assert new.last?
end
def test_reordering
assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
end
def test_insert_at
new = DefaultScopedWhereMixin.create
assert_equal 5, new.pos
new = DefaultScopedWhereMixin.create
assert_equal 6, new.pos
new = DefaultScopedWhereMixin.create
assert_equal 7, new.pos
new_noup = DefaultScopedWhereMixin.acts_as_list_no_update { DefaultScopedWhereMixin.create }
assert_equal_or_nil $default_position, new_noup.pos
new4 = DefaultScopedWhereMixin.create
assert_equal 8, new4.pos
new4.insert_at(2)
assert_equal 2, new4.pos
new.reload
assert_equal 8, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 3, new4.pos
new5 = DefaultScopedWhereMixin.create
assert_equal 9, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 4, new4.pos
new_noup.reload
assert_equal_or_nil $default_position, new_noup.pos
end
def test_find_or_create_doesnt_raise_deprecation_warning
assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
DefaultScopedWhereMixin.find_or_create_by(pos: 5)
end
end
def test_update_position
assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 2).first.set_list_position(4)
assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 2).first.set_list_position(2)
assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 1).first.set_list_position(4)
assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
DefaultScopedWhereMixin.for_active_false_tests.where(id: 1).first.set_list_position(1)
assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.for_active_false_tests.map(&:id)
end
end
class MultiDestroyTest < ActsAsListTestCase
def setup
setup_db
end
# example:
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, order: "position"
# accepts_nested_attributes_for :todo_items, allow_destroy: true
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list scope: :todo_list
# end
#
# Assume that there are three items.
# The user mark two items as deleted, click save button, form will be post:
#
# todo_list.todo_items_attributes = [
# {id: 1, _destroy: true},
# {id: 2, _destroy: true}
# ]
#
# Save toto_list, the position of item #3 should eql 1.
#
def test_destroy
new1 = DefaultScopedMixin.create
assert_equal 1, new1.pos
new2 = DefaultScopedMixin.create
assert_equal 2, new2.pos
new3 = DefaultScopedMixin.create
assert_equal 3, new3.pos
new4 = DefaultScopedMixin.create
assert_equal 4, new4.pos
new1.destroy
new2.destroy
new3.reload
new4.reload
assert_equal 1, new3.pos
assert_equal 2, new4.pos
DefaultScopedMixin.acts_as_list_no_update { new3.destroy }
new4.reload
assert_equal 2, new4.pos
end
end
#class TopAdditionMixin < Mixin
class TopAdditionTest < ActsAsListTestCase
include Shared::TopAddition
def setup
setup_db
super
end
end
class TopAdditionTestWithDefault < ActsAsListTestCase
include Shared::TopAddition
def setup
setup_db_with_default
super
end
end
class NoAdditionTest < ActsAsListTestCase
include Shared::NoAddition
def setup
setup_db
super
end
end
class MultipleListsTest < ActsAsListTestCase
def setup
setup_db
(1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 1}
(1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 2}
end
def test_check_scope_order
assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 1).order('pos').map(&:id)
assert_equal [5, 6, 7, 8], ListMixin.where(:parent_id => 2).order('pos').map(&:id)
ListMixin.find(4).update :parent_id => 2, :pos => 2
assert_equal [1, 2, 3], ListMixin.where(:parent_id => 1).order('pos').map(&:id)
assert_equal [5, 4, 6, 7, 8], ListMixin.where(:parent_id => 2).order('pos').map(&:id)
end
def test_check_scope_position
assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 1).map(&:pos)
assert_equal [1, 2, 3, 4], ListMixin.where(:parent_id => 2).map(&:pos)
ListMixin.find(4).update :parent_id => 2, :pos => 2
assert_equal [1, 2, 3], ListMixin.where(:parent_id => 1).order('pos').map(&:pos)
assert_equal [1, 2, 3, 4, 5], ListMixin.where(:parent_id => 2).order('pos').map(&:pos)
end
def test_find_or_create_doesnt_raise_deprecation_warning
assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
ListMixin.where(:parent_id => 1).find_or_create_by(pos: 5)
end
end
end
class EnumArrayScopeListMixinTest < ActsAsListTestCase
def setup
setup_db
EnumArrayScopeListMixin.create! :parent_id => 1, :state => EnumArrayScopeListMixin.states['active']
EnumArrayScopeListMixin.create! :parent_id => 1, :state => EnumArrayScopeListMixin.states['archived']
EnumArrayScopeListMixin.create! :parent_id => 2, :state => EnumArrayScopeListMixin.states["active"]
EnumArrayScopeListMixin.create! :parent_id => 2, :state => EnumArrayScopeListMixin.states["archived"]
end
def test_positions
assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 1, :state => EnumArrayScopeListMixin.states['active']).map(&:pos)
assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 1, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos)
assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['active']).map(&:pos)
assert_equal [1], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos)
end
def test_update_state
active_item = EnumArrayScopeListMixin.find_by(:parent_id => 2, :state => EnumArrayScopeListMixin.states['active'])
active_item.update(state: EnumArrayScopeListMixin.states['archived'])
assert_equal [1, 2], EnumArrayScopeListMixin.where(:parent_id => 2, :state => EnumArrayScopeListMixin.states['archived']).map(&:pos).sort
end
end
class MultipleListsArrayScopeTest < ActsAsListTestCase
def setup
setup_db
(1..4).each { |counter| ArrayScopeListMixin.create! :pos => counter,:parent_id => 1, :parent_type => 'anything'}
(1..4).each { |counter| ArrayScopeListMixin.create! :pos => counter,:parent_id => 2, :parent_type => 'something'}
(1..4).each { |counter| ArrayScopeListMixin.create! :pos => counter,:parent_id => 3, :parent_type => 'anything'}
end
def test_order_after_all_scope_properties_are_changed
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:id)
assert_equal [5, 6, 7, 8], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').order('pos').map(&:id)
ArrayScopeListMixin.find(2).update :parent_id => 2, :pos => 2,:parent_type => 'something'
assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order('pos').map(&:id)
assert_equal [5, 2, 6, 7, 8], ArrayScopeListMixin.where(:parent_id => 2,:parent_type => 'something').order('pos').map(&:id)
end
def test_position_after_all_scope_properties_are_changed
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').map(&:pos)
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').map(&:pos)
ArrayScopeListMixin.find(4).update :parent_id => 2, :pos => 2, :parent_type => 'something'
assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:pos)
assert_equal [1, 2, 3, 4, 5], ArrayScopeListMixin.where(:parent_id => 2, :parent_type => 'something').order('pos').map(&:pos)
end
def test_order_after_one_scope_property_is_changed
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:id)
assert_equal [9, 10, 11, 12], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').order('pos').map(&:id)
ArrayScopeListMixin.find(2).update :parent_id => 3, :pos => 2
assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order('pos').map(&:id)
assert_equal [9, 2, 10, 11, 12], ArrayScopeListMixin.where(:parent_id => 3,:parent_type => 'anything').order('pos').map(&:id)
end
def test_position_after_one_scope_property_is_changed
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').map(&:pos)
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').map(&:pos)
ArrayScopeListMixin.find(4).update :parent_id => 3, :pos => 2
assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:pos)
assert_equal [1, 2, 3, 4, 5], ArrayScopeListMixin.where(:parent_id => 3, :parent_type => 'anything').order('pos').map(&:pos)
end
def test_order_after_moving_to_empty_list
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:id)
assert_equal [], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order('pos').map(&:id)
ArrayScopeListMixin.find(2).update :parent_id => 4, :pos => 1
assert_equal [1, 3, 4], ArrayScopeListMixin.where(:parent_id => 1,:parent_type => 'anything').order('pos').map(&:id)
assert_equal [2], ArrayScopeListMixin.where(:parent_id => 4,:parent_type => 'anything').order('pos').map(&:id)
end
def test_position_after_moving_to_empty_list
assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').map(&:pos)
assert_equal [], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').map(&:pos)
ArrayScopeListMixin.find(2).update :parent_id => 4, :pos => 1
assert_equal [1, 2, 3], ArrayScopeListMixin.where(:parent_id => 1, :parent_type => 'anything').order('pos').map(&:pos)
assert_equal [1], ArrayScopeListMixin.where(:parent_id => 4, :parent_type => 'anything').order('pos').map(&:pos)
end
end
class ArrayScopeListWithHashTest
def setup
setup_db
@obj1 = ArrayScopeListWithHashMixin.create! :pos => counter, :parent_id => 1, :state => nil
@obj2 = ArrayScopeListWithHashMixin.create! :pos => counter, :parent_id => 1, :state => 'anything'
end
def test_scope_condition_correct
[@obj1, @obj2].each do |obj|
assert_equal({ :parent_id => 1, :state => nil }, obj.scope_condition)
end
end
end
require 'timecop'
class TouchTest < ActsAsListTestCase
def setup
setup_db
Timecop.freeze(yesterday) do
4.times { ListMixin.create! }
end
end
def now
@now ||= Time.current.change(usec: 0)
end
def yesterday
@yesterday ||= 1.day.ago
end
def updated_ats
ListMixin.order(:id).pluck(:updated_at)
end
def test_moving_item_lower_touches_self_and_lower_item
Timecop.freeze(now) do
ListMixin.first.move_lower
updated_ats[0..1].each do |updated_at|
assert_equal updated_at.to_i, now.to_i
end
updated_ats[2..3].each do |updated_at|
assert_equal updated_at.to_i, yesterday.to_i
end
end
end
def test_moving_item_higher_touches_self_and_higher_item
Timecop.freeze(now) do
ListMixin.all.second.move_higher
updated_ats[0..1].each do |updated_at|
assert_equal updated_at.to_i, now.to_i
end
updated_ats[2..3].each do |updated_at|
assert_equal updated_at.to_i, yesterday.to_i
end
end
end
def test_moving_item_to_bottom_touches_all_other_items
Timecop.freeze(now) do
ListMixin.first.move_to_bottom
updated_ats.each do |updated_at|
assert_equal updated_at.to_i, now.to_i
end
end
end
def test_moving_item_to_top_touches_all_other_items
Timecop.freeze(now) do
ListMixin.last.move_to_top
updated_ats.each do |updated_at|
assert_equal updated_at.to_i, now.to_i
end
end
end
def test_removing_item_touches_all_lower_items
Timecop.freeze(now) do
ListMixin.all.third.remove_from_list
updated_ats[0..1].each do |updated_at|
assert_equal updated_at.to_i, yesterday.to_i
end
updated_ats[2..2].each do |updated_at|
assert_equal updated_at.to_i, now.to_i
end
end
end
end
class TouchDisabledTest < ActsAsListTestCase
def setup
setup_db
Timecop.freeze(yesterday) do
4.times { TouchDisabledMixin.create! }
end
end
def now
@now ||= Time.current.change(usec: 0)
end
def yesterday
@yesterday ||= 1.day.ago
end
def updated_ats
TouchDisabledMixin.order(:id).pluck(:updated_at)
end
def positions
ListMixin.order(:id).pluck(:pos)
end
def test_deleting_item_does_not_touch_higher_items
Timecop.freeze(now) do
TouchDisabledMixin.first.destroy
updated_ats.each do |updated_at|
assert_equal updated_at.to_i, yesterday.to_i
end
assert_equal positions, [1, 2, 3]
end
end
end
class ActsAsListTopTest < ActsAsListTestCase
def setup
setup_db
end
def test_acts_as_list_top
assert_equal 1, TheBaseSubclass.new.acts_as_list_top
assert_equal 0, ZeroBasedMixin.new.acts_as_list_top
end
def test_class_acts_as_list_top
assert_equal 1, TheBaseSubclass.acts_as_list_top
assert_equal 0, ZeroBasedMixin.acts_as_list_top
end
end
class NilPositionTest < ActsAsListTestCase
def setup
setup_db
end
def test_nil_position_ordering
new1 = DefaultScopedMixin.create pos: nil
new2 = DefaultScopedMixin.create pos: nil
new3 = DefaultScopedMixin.create pos: nil
DefaultScopedMixin.update_all(pos: nil)
assert_equal [nil, nil, nil], DefaultScopedMixin.all.map(&:pos)
new1.reload.pos = 1
new1.save
new3.reload.pos = 1
new3.save
assert_equal [1, 2], DefaultScopedMixin.where("pos IS NOT NULL").map(&:pos)
assert_equal [3, 1], DefaultScopedMixin.where("pos IS NOT NULL").map(&:id)
assert_nil new2.reload.pos
new2.reload.pos = 1
new2.save
assert_equal [1, 2, 3], DefaultScopedMixin.all.map(&:pos)
assert_equal [2, 3, 1], DefaultScopedMixin.all.map(&:id)
end
end
class SequentialUpdatesOptionDefaultTest < ActsAsListTestCase
def setup
setup_db
end
def test_sequential_updates_default_to_false_without_unique_index
assert_equal false, SequentialUpdatesDefault.new.send(:sequential_updates?)
end
end
class SequentialUpdatesMixinNotNullUniquePositiveConstraintsTest < ActsAsListTestCase
def setup
setup_db null: false, unique: true, positive: true
(1..4).each { |counter| SequentialUpdatesDefault.create!({pos: counter}) }
end
def test_sequential_updates_default_to_true_with_unique_index
assert_equal true, SequentialUpdatesDefault.new.send(:sequential_updates?)
end
def test_sequential_updates_option_override_with_false
assert_equal false, SequentialUpdatesFalseMixin.new.send(:sequential_updates?)
end
def test_insert_at
new = SequentialUpdatesDefault.create
assert_equal 5, new.pos
new.insert_at(1)
assert_equal 1, new.pos
new.insert_at(5)
assert_equal 5, new.pos
new.insert_at(3)
assert_equal 3, new.pos
end
def test_move_to_bottom
item = SequentialUpdatesDefault.order(:pos).first
item.move_to_bottom
assert_equal 4, item.pos
end
def test_move_to_top
new_item = SequentialUpdatesDefault.create!
assert_equal 5, new_item.pos
new_item.move_to_top
assert_equal 1, new_item.pos
end
def test_destroy
new_item = SequentialUpdatesDefault.create
assert_equal 5, new_item.pos
new_item.insert_at(2)
assert_equal 2, new_item.pos
new_item.destroy
assert_equal [1,2,3,4], SequentialUpdatesDefault.all.map(&:pos).sort
end
def test_exception_on_wrong_position
new_item = SequentialUpdatesDefault.create
assert_raises ArgumentError do
new_item.insert_at(0)
end
end
class SequentialUpdatesMixinNotNullUniquePositiveConstraintsTest < ActsAsListTestCase
def setup
setup_db null: false, unique: true, positive: true
(1..4).each { |counter| SequentialUpdatesAltId.create!({pos: counter}) }
end
def test_sequential_updates_default_to_true_with_unique_index
assert_equal true, SequentialUpdatesAltId.new.send(:sequential_updates?)
end
def test_insert_at
new = SequentialUpdatesAltId.create
assert_equal 5, new.pos
new.insert_at(1)
assert_equal 1, new.pos
new.insert_at(5)
assert_equal 5, new.pos
new.insert_at(3)
assert_equal 3, new.pos
end
def test_create_at_top
new = SequentialUpdatesAltId.create!(pos: 1)
assert_equal 1, new.pos
end
def test_move_to_bottom
item = SequentialUpdatesAltId.order(:pos).first
item.move_to_bottom
assert_equal 4, item.pos
end
def test_move_to_top
new_item = SequentialUpdatesAltId.create!
assert_equal 5, new_item.pos
new_item.move_to_top
assert_equal 1, new_item.pos
end
def test_destroy
new_item = SequentialUpdatesAltId.create
assert_equal 5, new_item.pos
new_item.insert_at(2)
assert_equal 2, new_item.pos
new_item.destroy
assert_equal [1,2,3,4], SequentialUpdatesAltId.all.map(&:pos).sort
end
end
class SequentialUpdatesAltIdTouchDisabledTest < ActsAsListTestCase
def setup
setup_db
Timecop.freeze(yesterday) do
4.times { SequentialUpdatesAltIdTouchDisabled.create! }
end
end
def now
@now ||= Time.current.change(usec: 0)
end
def yesterday
@yesterday ||= 1.day.ago
end
def updated_ats
SequentialUpdatesAltIdTouchDisabled.order(:altid).pluck(:updated_at)
end
def positions
SequentialUpdatesAltIdTouchDisabled.order(:altid).pluck(:pos)
end
def test_sequential_updates_default_to_true_with_unique_index
assert_equal true, SequentialUpdatesAltIdTouchDisabled.new.send(:sequential_updates?)
end
def test_deleting_item_does_not_touch_higher_items
Timecop.freeze(now) do
SequentialUpdatesAltIdTouchDisabled.first.destroy
updated_ats.each do |updated_at|
assert_equal updated_at.to_i, yesterday.to_i
end
assert_equal positions, [1, 2, 3]
end
end
end
end
acts_as_list-1.0.4/test/test_default_scope_with_select.rb 0000644 0000041 0000041 00000001356 14042233640 023771 0 ustar www-data www-data require 'helper'
class Animal < ActiveRecord::Base
acts_as_list
default_scope -> { select(:name) }
end
class DefaultScopeWithSelectTest < Minitest::Test
def setup
ActiveRecord::Base.connection.create_table :animals do |t|
t.column :position, :integer
t.column :name, :string
end
ActiveRecord::Base.connection.schema_cache.clear!
Animal.reset_column_information
super
end
def teardown
teardown_db
super
end
def test_default_scope_with_select
animal1 = Animal.create name: 'Fox'
animal2 = Animal.create name: 'Panda'
animal3 = Animal.create name: 'Wildebeast'
assert_equal 1, animal1.position
assert_equal 2, animal2.position
assert_equal 3, animal3.position
end
end
acts_as_list-1.0.4/test/test_no_update_for_scope_destruction.rb 0000644 0000041 0000041 00000004164 14042233640 025222 0 ustar www-data www-data # frozen_string_literal: true
require 'helper'
class DestructionTodoList < ActiveRecord::Base
has_many :destruction_todo_items, dependent: :destroy
has_many :destruction_tada_items, dependent: :destroy
end
class DestructionTodoItem < ActiveRecord::Base
belongs_to :destruction_todo_list
acts_as_list scope: :destruction_todo_list
end
class DestructionTadaItem < ActiveRecord::Base
belongs_to :destruction_todo_list
acts_as_list scope: [:destruction_todo_list_id, :enabled]
end
class NoUpdateForScopeDestructionTestCase < Minitest::Test
def setup
ActiveRecord::Base.connection.create_table :destruction_todo_lists do |t|
end
ActiveRecord::Base.connection.create_table :destruction_todo_items do |t|
t.column :position, :integer
t.column :destruction_todo_list_id, :integer
end
ActiveRecord::Base.connection.create_table :destruction_tada_items do |t|
t.column :position, :integer
t.column :destruction_todo_list_id, :integer
t.column :enabled, :boolean
end
ActiveRecord::Base.connection.schema_cache.clear!
[DestructionTodoList, DestructionTodoItem, DestructionTadaItem].each(&:reset_column_information)
super
end
def teardown
teardown_db
super
end
class NoUpdateForScopeDestructionTest < NoUpdateForScopeDestructionTestCase
def setup
super
@list = DestructionTodoList.create!
@todo_item_1 = DestructionTodoItem.create! position: 1, destruction_todo_list_id: @list.id
@tada_item_1 = DestructionTadaItem.create! position: 1, destruction_todo_list_id: @list.id, enabled: true
end
def test_no_update_children_when_parent_destroyed
DestructionTodoItem.any_instance.expects(:decrement_positions_on_lower_items).never
DestructionTadaItem.any_instance.expects(:decrement_positions_on_lower_items).never
assert @list.destroy
end
def test_update_children_when_sibling_destroyed
@todo_item_1.expects(:decrement_positions_on_lower_items).once
@tada_item_1.expects(:decrement_positions_on_lower_items).once
assert @todo_item_1.destroy
assert @tada_item_1.destroy
end
end
end
acts_as_list-1.0.4/test/test_scope_with_user_defined_foreign_key.rb 0000644 0000041 0000041 00000002217 14042233640 026020 0 ustar www-data www-data require 'helper'
class Checklist < ActiveRecord::Base
has_many :checklist_items, foreign_key: 'list_id', inverse_of: :checklist
end
class ChecklistItem < ActiveRecord::Base
belongs_to :checklist, foreign_key: 'list_id', inverse_of: :checklist_items
acts_as_list scope: :checklist
end
class ScopeWithUserDefinedForeignKeyTest < Minitest::Test
def setup
ActiveRecord::Base.connection.create_table :checklists do |t|
end
ActiveRecord::Base.connection.create_table :checklist_items do |t|
t.column :list_id, :integer
t.column :position, :integer
end
ActiveRecord::Base.connection.schema_cache.clear!
[Checklist, ChecklistItem].each(&:reset_column_information)
super
end
def teardown
teardown_db
super
end
def test_scope_with_user_defined_foreign_key
checklist = Checklist.create
checklist_item_1 = checklist.checklist_items.create
checklist_item_2 = checklist.checklist_items.create
checklist_item_3 = checklist.checklist_items.create
assert_equal 1, checklist_item_1.position
assert_equal 2, checklist_item_2.position
assert_equal 3, checklist_item_3.position
end
end
acts_as_list-1.0.4/test/shared_no_addition.rb 0000644 0000041 0000041 00000001544 14042233640 021337 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module NoAddition
def setup
(1..4).each { |counter| NoAdditionMixin.create! pos: counter, parent_id: 5 }
end
def test_insert
new = NoAdditionMixin.create(parent_id: 20)
assert_nil new.pos
assert !new.in_list?
new = NoAdditionMixin.create(parent_id: 20)
assert_nil new.pos
end
def test_update_does_not_add_to_list
new = NoAdditionMixin.create(parent_id: 20)
new.update_attribute(:updated_at, Time.now) # force some change
new.reload
assert !new.in_list?
end
def test_update_scope_does_not_add_to_list
new = NoAdditionMixin.create
new.update_attribute(:parent_id, 20)
new.reload
assert !new.in_list?
new.update_attribute(:parent_id, 5)
new.reload
assert !new.in_list?
end
end
end
acts_as_list-1.0.4/test/shared.rb 0000644 0000041 0000041 00000000632 14042233640 016765 0 ustar www-data www-data # frozen_string_literal: true
# Common shared behaviour.
module Shared
autoload :List, 'shared_list'
autoload :ListSub, 'shared_list_sub'
autoload :ZeroBased, 'shared_zero_based'
autoload :ArrayScopeList, 'shared_array_scope_list'
autoload :TopAddition, 'shared_top_addition'
autoload :NoAddition, 'shared_no_addition'
autoload :Quoting, 'shared_quoting'
end
acts_as_list-1.0.4/test/shared_list.rb 0000644 0000041 0000041 00000024777 14042233640 020040 0 ustar www-data www-data # frozen_string_literal: true
module Shared
module List
def setup
(1..4).each do |counter|
node = ListMixin.new parent_id: 5
node.pos = counter
node.save!
end
end
def test_current_position
first_item = ListMixin.where(parent_id: 5).first
assert_equal 1, first_item.current_position
first_item.remove_from_list
assert_nil first_item.current_position
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_lower
assert_equal [1, 3, 2, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_higher
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 1).first.move_to_top
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 4).first.move_to_top
assert_equal [4, 1, 3, 2], ListMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 3).first.move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.where(id: 2).first, ListMixin.where(id: 1).first.lower_item
assert_nil ListMixin.where(id: 1).first.higher_item
assert_equal ListMixin.where(id: 3).first, ListMixin.where(id: 4).first.higher_item
assert_nil ListMixin.where(id: 4).first.lower_item
end
def test_injection
item = ListMixin.new(parent_id: 1)
assert_equal({ parent_id: 1 }, item.scope_condition)
assert_equal "pos", item.position_column
end
def test_insert
new = ListMixin.create(parent_id: 20)
assert_equal 1, new.pos
assert new.first?
assert new.last?
new = ListMixin.create(parent_id: 20)
assert_equal 2, new.pos
assert !new.first?
assert new.last?
new = ListMixin.acts_as_list_no_update { ListMixin.create(parent_id: 20) }
assert_equal_or_nil $default_position, new.pos
assert_equal $default_position.is_a?(Integer), new.first?
assert !new.last?
new = ListMixin.create(parent_id: 20)
assert_equal 3, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(parent_id: 0)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_insert_at
new = ListMixin.create(parent_id: 20)
assert_equal 1, new.pos
new = ListMixin.create(parent_id: 20)
assert_equal 2, new.pos
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_or_nil $default_position, new_noup.pos
new4 = ListMixin.create(parent_id: 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixin.create(parent_id: 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
new_noup.reload
assert_equal_or_nil $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)
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_insert_at_after_dup
new1 = ListMixin.create(parent_id: 20)
new2 = ListMixin.create(parent_id: 20)
new3 = ListMixin.create(parent_id: 20)
duped = new1.dup
duped.save
[new1, new2, new3, duped].map(&:reload)
assert_equal [1, 2, 3, 4], [duped.pos, new1.pos, new2.pos, new3.pos]
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)
assert_equal 1, ListMixin.where(id: 1).first.pos
assert_equal 2, ListMixin.where(id: 3).first.pos
assert_equal 3, ListMixin.where(id: 4).first.pos
ListMixin.where(id: 1).first.destroy
assert_equal [3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
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
new = ListWithStringScopeMixin.create(parent_id: 500)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_nil_scope
new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
new2.move_higher
assert_equal [new2, new1, new3].map(&:id), ListMixin.where(parent_id: nil).order('pos').map(&:id)
end
def test_update_position_when_scope_changes
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.create(parent_id: 6)
ListMixin.where(id: 2).first.move_within_scope(6)
assert_equal 2, ListMixin.where(id: 2).first.pos
assert_equal [1, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
assert_equal 1, ListMixin.where(id: 1).first.pos
assert_equal 2, ListMixin.where(id: 3).first.pos
assert_equal 3, ListMixin.where(id: 4).first.pos
ListMixin.where(id: 2).first.move_within_scope(5)
assert_equal [1, 3, 4, 2], ListMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_remove_from_list_should_then_fail_in_list?
assert_equal true, ListMixin.where(id: 1).first.in_list?
ListMixin.where(id: 1).first.remove_from_list
assert_equal false, ListMixin.where(id: 1).first.in_list?
end
def test_remove_from_list_should_set_position_to_nil
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.remove_from_list
assert_equal 1, ListMixin.where(id: 1).first.pos
assert_nil ListMixin.where(id: 2).first.pos
assert_equal 2, ListMixin.where(id: 3).first.pos
assert_equal 3, ListMixin.where(id: 4).first.pos
end
def test_remove_before_destroy_does_not_shift_lower_items_twice
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
ListMixin.where(id: 2).first.remove_from_list
ListMixin.where(id: 2).first.destroy
assert_equal [1, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
assert_equal 1, ListMixin.where(id: 1).first.pos
assert_equal 2, ListMixin.where(id: 3).first.pos
assert_equal 3, ListMixin.where(id: 4).first.pos
end
def test_before_destroy_callbacks_do_not_update_position_to_nil_before_deleting_the_record
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
# We need to trigger all the before_destroy callbacks without actually
# destroying the record so we can see the affect the callbacks have on
# the record.
list = ListMixin.where(id: 2).first
if list.respond_to?(:run_callbacks)
list.run_callbacks(:destroy)
else
list.send(:callback, :before_destroy)
end
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
assert_equal 1, ListMixin.where(id: 1).first.pos
assert_equal 2, ListMixin.where(id: 2).first.pos
assert_equal 2, ListMixin.where(id: 3).first.pos
assert_equal 3, ListMixin.where(id: 4).first.pos
end
def test_before_create_callback_adds_to_bottom
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
new = ListMixin.create(parent_id: 5)
assert_equal 5, new.pos
assert !new.first?
assert new.last?
assert_equal [1, 2, 3, 4, 5], ListMixin.where(parent_id: 5).order('pos').map(&:id)
end
def test_before_create_callback_adds_to_given_position
assert_equal [1, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos').map(&:id)
new = ListMixin.new(parent_id: 5)
new.pos = 1
new.save!
assert_equal 1, new.pos
assert new.first?
assert !new.last?
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
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, 7, 2, 3, 4], ListMixin.where(parent_id: 5).order('pos, id').map(&:id)
end
def test_non_persisted_records_dont_get_lock_called
new = ListMixin.new(parent_id: 5)
new.destroy
end
def test_invalid_records_dont_get_inserted
new = ListMixinError.new(parent_id: 5, state: nil)
assert !new.valid?
new.insert_at(1)
assert !new.persisted?
end
def test_invalid_records_raise_error_with_insert_at!
new = ListMixinError.new(parent_id: 5, state: nil)
assert !new.valid?
assert_raises ActiveRecord::RecordInvalid do
new.insert_at!(1)
end
end
def test_find_or_create_doesnt_raise_deprecation_warning
assert_no_deprecation_warning_raised_by('ActiveRecord deprecation warning raised when using `find_or_create_by` when we didn\'t expect it') do
ListMixin.where(parent_id: 5).find_or_create_by(pos: 5)
end
end
end
end
acts_as_list-1.0.4/test/test_no_update_for_subclasses.rb 0000644 0000041 0000041 00000002500 14042233640 023625 0 ustar www-data www-data # frozen_string_literal: true
require 'helper'
class MasterItem < ActiveRecord::Base
acts_as_list
end
class SlaveItem < MasterItem; end
class NoUpdateForSubclassesTestCase < Minitest::Test
def setup
ActiveRecord::Base.connection.create_table :master_items do |t|
t.column :position, :integer
t.column :type, :string
end
ActiveRecord::Base.connection.schema_cache.clear!
[MasterItem, SlaveItem].each(&:reset_column_information)
super
end
def teardown
teardown_db
super
end
end
class NoUpdateForSubclassesTest < NoUpdateForSubclassesTestCase
def setup
super
@item_1, @item_2 = (1..2).map do |counter|
SlaveItem.create!(position: counter)
end
end
def test_update
@item_1.update position: 2
assert_equal 2, @item_1.reload.position
assert_equal 1, @item_2.reload.position
end
def test_no_update_for_subclass_instances_with_no_update_on_superclass
MasterItem.acts_as_list_no_update { @item_1.update position: 2 }
assert_equal 2, @item_1.reload.position
assert_equal 2, @item_2.reload.position
end
def test_no_update_for_subclass_instances_with_no_update_on_subclass
SlaveItem.acts_as_list_no_update { @item_1.update position: 2 }
assert_equal 2, @item_1.reload.position
assert_equal 2, @item_2.reload.position
end
end
acts_as_list-1.0.4/test/database.yml 0000644 0000041 0000041 00000000421 14042233640 017455 0 ustar www-data www-data sqlite:
adapter: sqlite3
database: "file:memdb1?mode=memory&cache=shared"
mysql:
adapter: mysql2
username: root
password:
database: acts_as_list
postgresql:
adapter: postgresql
username: postgres
password:
database: acts_as_list
min_messages: ERROR
acts_as_list-1.0.4/README.md 0000644 0000041 0000041 00000033776 14042233640 015511 0 ustar www-data www-data # Acts As List
## Build Status
[](https://travis-ci.org/brendon/acts_as_list)
[](https://badge.fury.io/rb/acts_as_list)
## Description
This `acts_as` extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a `position` column defined as an integer on the mapped database table.
## 0.8.0 Upgrade Notes
There are a couple of changes of behaviour from `0.8.0` onwards:
- If you specify `add_new_at: :top`, new items will be added to the top of the list like always. But now, if you specify a position at insert time: `.create(position: 3)`, the position will be respected. In this example, the item will end up at position `3` and will move other items further down the list. Before `0.8.0` the position would be ignored and the item would still be added to the top of the list. [#220](https://github.com/brendon/acts_as_list/pull/220)
- `acts_as_list` now copes with disparate position integers (i.e. gaps between the numbers). There has been a change in behaviour for the `higher_items` method. It now returns items with the first item in the collection being the closest item to the reference item, and the last item in the collection being the furthest from the reference item (a.k.a. the first item in the list). [#223](https://github.com/brendon/acts_as_list/pull/223)
## Installation
In your Gemfile:
gem 'acts_as_list'
Or, from the command line:
gem install acts_as_list
## Example
At first, you need to add a `position` column to desired table:
rails g migration AddPositionToTodoItem position:integer
rake db:migrate
After that you can use `acts_as_list` method in the model:
```ruby
class TodoList < ActiveRecord::Base
has_many :todo_items, -> { order(position: :asc) }
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
acts_as_list scope: :todo_list
end
todo_list = TodoList.find(...)
todo_list.todo_items.first.move_to_bottom
todo_list.todo_items.last.move_higher
```
## Instance Methods Added To ActiveRecord Models
You'll have a number of methods added to each instance of the ActiveRecord model that to which `acts_as_list` is added.
In `acts_as_list`, "higher" means further up the list (a lower `position`), and "lower" means further down the list (a higher `position`). That can be confusing, so it might make sense to add tests that validate that you're using the right method given your context.
### Methods That Change Position and Reorder List
- `list_item.insert_at(2)`
- `list_item.move_lower` will do nothing if the item is the lowest item
- `list_item.move_higher` will do nothing if the item is the highest item
- `list_item.move_to_bottom`
- `list_item.move_to_top`
- `list_item.remove_from_list`
### Methods That Change Position Without Reordering List
- `list_item.increment_position`
- `list_item.decrement_position`
- `list_item.set_list_position(3)`
### Methods That Return Attributes of the Item's List Position
- `list_item.first?`
- `list_item.last?`
- `list_item.in_list?`
- `list_item.not_in_list?`
- `list_item.default_position?`
- `list_item.higher_item`
- `list_item.higher_items` will return all the items above `list_item` in the list (ordered by the position, ascending)
- `list_item.lower_item`
- `list_item.lower_items` will return all the items below `list_item` in the list (ordered by the position, ascending)
## Adding `acts_as_list` To An Existing Model
As it stands `acts_as_list` requires position values to be set on the model before the instance methods above will work. Adding something like the below to your migration will set the default position. Change the parameters to order if you want a different initial ordering.
```ruby
class AddPositionToTodoItem < ActiveRecord::Migration
def change
add_column :todo_items, :position, :integer
TodoItem.order(:updated_at).each.with_index(1) do |todo_item, index|
todo_item.update_column :position, index
end
end
end
```
If you are using the scope option things can get a bit more complicated. Let's say you have `acts_as_list scope: :todo_list`, you might instead need something like this:
```ruby
TodoList.all.each do |todo_list|
todo_list.todo_items.order(:updated_at).each.with_index(1) do |todo_item, index|
todo_item.update_column :position, index
end
end
```
When using PostgreSQL, it is also possible to leave this migration up to the database layer. Inside of the `change` block you could write:
```ruby
execute <<~SQL.squish
UPDATE todo_items
SET position = mapping.new_position
FROM (
SELECT
id,
ROW_NUMBER() OVER (
PARTITION BY todo_list_id
ORDER BY updated_at
) as new_position
FROM todo_items
) AS mapping
WHERE todo_items.id = mapping.id;
SQL
```
## Notes
All `position` queries (select, update, etc.) inside gem methods are executed without the default scope (i.e. `Model.unscoped`), this will prevent nasty issues when the default scope is different from `acts_as_list` scope.
The `position` column is set after validations are called, so you should not put a `presence` validation on the `position` column.
If you need a scope by a non-association field you should pass an array, containing field name, to a scope:
```ruby
class TodoItem < ActiveRecord::Base
# `kind` is a plain text field (e.g. 'work', 'shopping', 'meeting'), not an association
acts_as_list scope: [:kind]
end
```
You can also add multiple scopes in this fashion:
```ruby
class TodoItem < ActiveRecord::Base
acts_as_list scope: [:kind, :owner_id]
end
```
Furthermore, you can optionally include a hash of fixed parameters that will be included in all queries:
```ruby
class TodoItem < ActiveRecord::Base
acts_as_list scope: [:kind, :owner_id, deleted_at: nil]
end
```
This is useful when using this gem in conjunction with the popular [acts_as_paranoid](https://github.com/ActsAsParanoid/acts_as_paranoid) gem.
## More Options
- `column`
default: `position`. Use this option if the column name in your database is different from position.
- `top_of_list`
default: `1`. Use this option to define the top of the list. Use 0 to make the collection act more like an array in its indexing.
- `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.
- `touch_on_update`
default: `true`. Use `touch_on_update: false` if you don't want to update the timestamps of the associated records.
- `sequential_updates`
Specifies whether insert_at should update objects positions during shuffling one by one to respect position column unique not null constraint. Defaults to true if position column has unique index, otherwise false. If constraint is `deferrable initially deferred` (PostgreSQL), overriding it with false will speed up insert_at.
## 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.
You can also pass an array of classes as an argument to disable database updates on just those classes. It can be any ActiveRecord class that has acts_as_list enabled.
```ruby
class TodoList < ActiveRecord::Base
has_many :todo_items, -> { order(position: :asc) }
acts_as_list
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
has_many :todo_attachments, -> { order(position: :asc) }
acts_as_list scope: :todo_list
end
class TodoAttachment < ActiveRecord::Base
belongs_to :todo_list
acts_as_list scope: :todo_item
end
TodoItem.acts_as_list_no_update([TodoAttachment]) do
TodoItem.find(10).update(position: 2)
TodoAttachment.find(10).update(position: 1)
TodoAttachment.find(11).update(position: 2)
TodoList.find(2).update(position: 3) # For this instance the callbacks will be called because we haven't passed the class as an argument
end
```
## Troubleshooting Database Deadlock Errors
When using this gem in an app with a high amount of concurrency, you may see "deadlock" errors raised by your database server.
It's difficult for the gem to provide a solution that fits every app.
Here are some steps you can take to mitigate and handle these kinds of errors.
### 1) Use the Most Concise API
One easy way to reduce deadlocks is to use the most concise gem API available for what you want to accomplish.
In this specific example, the more concise API for creating a list item at a position results in one transaction instead of two,
and it issues fewer SQL statements. Issuing fewer statements tends to lead to faster transactions.
Faster transactions are less likely to deadlock.
Example:
```ruby
# Good
TodoItem.create(todo_list: todo_list, position: 1)
# Bad
item = TodoItem.create(todo_list: todo_list)
item.insert_at(1)
```
### 2) Rescue then Retry
Deadlocks are always a possibility when updating tables rows concurrently.
The general advice from MySQL documentation is to catch these errors and simply retry the transaction; it will probably succeed on another attempt. (see [How to Minimize and Handle Deadlocks](https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html))
Retrying transactions sounds simple, but there are many details that need to be chosen on a per-app basis:
How many retry attempts should be made?
Should there be a wait time between attempts?
What _other_ statements were in the transaction that got rolled back?
Here a simple example of rescuing from deadlock and retrying the operation:
* `ActiveRecord::Deadlocked` is available in Rails >= 5.1.0.
* If you have Rails < 5.1.0, you will need to rescue `ActiveRecord::StatementInvalid` and check `#cause`.
```ruby
attempts_left = 2
while attempts_left > 0
attempts_left -= 1
begin
TodoItem.transaction do
TodoItem.create(todo_list: todo_list, position: 1)
end
attempts_left = 0
rescue ActiveRecord::Deadlocked
raise unless attempts_left > 0
end
end
```
You can also use the approach suggested in this StackOverflow post:
https://stackoverflow.com/questions/4027659/activerecord3-deadlock-retry
### 3) Lock Parent Record
In addition to reacting to deadlocks, it is possible to reduce their frequency with more pessimistic locking.
This approach uses the parent record as a mutex for the entire list.
This kind of locking is very effective at reducing the frequency of deadlocks while updating list items.
However, there are some things to keep in mind:
* This locking pattern needs to be used around *every* call that modifies the list; even if it does not reorder list items.
* This locking pattern effectively serializes operations on the list. The throughput of operations on the list will decrease.
* Locking the parent record may lead to deadlock elsewhere if some other code also locks the parent table.
Example:
```ruby
todo_list = TodoList.create(name: "The List")
todo_list.with_lock do
item = TodoItem.create(description: "Buy Groceries", todo_list: todo_list, position: 1)
end
```
## Versions
Version `0.9.0` adds `acts_as_list_no_update` (https://github.com/brendon/acts_as_list/pull/244) and compatibility with not-null and uniqueness constraints on the database (https://github.com/brendon/acts_as_list/pull/246). These additions shouldn't break compatibility with existing implementations.
As of version `0.7.5` Rails 5 is supported.
All versions `0.1.5` onwards require Rails 3.0.x and higher.
## A note about data integrity
We often hear complaints that `position` values are repeated, incorrect etc. For example, #254. To ensure data integrity, you should rely on your database. There are two things you can do:
1. Use constraints. If you model `Item` that `belongs_to` an `Order`, and it has a `position` column, then add a unique constraint on `items` with `[:order_id, :position]`. Think of it as a list invariant. What are the properties of your list that don't change no matter how many items you have in it? One such propery is that each item has a distinct position. Another _could be_ that position is always greater than 0. It is strongly recommended that you rely on your database to enforce these invariants or constraints. Here are the docs for [PostgreSQL](https://www.postgresql.org/docs/9.5/static/ddl-constraints.html) and [MySQL](https://dev.mysql.com/doc/refman/8.0/en/alter-table.html).
2. Use mutexes or row level locks. At its heart the duplicate problem is that of handling concurrency. Adding a contention resolution mechanism like locks will solve it to some extent. But it is not a solution or replacement for constraints. Locks are also prone to deadlocks.
As a library, `acts_as_list` may not always have all the context needed to apply these tools. They are much better suited at the application level.
## Roadmap
1. Sort based feature
## Contributing to `acts_as_list`
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
- Fork the project
- Start a feature/bugfix branch
- Commit and push until you are happy with your contribution
- Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
- Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
- I would recommend using Rails 3.1.x and higher for testing the build before a pull request. The current test harness does not quite work with 3.0.x. The plugin itself works, but the issue lies with testing infrastructure.
## Copyright
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
acts_as_list-1.0.4/gemfiles/ 0000755 0000041 0000041 00000000000 14042233640 016005 5 ustar www-data www-data acts_as_list-1.0.4/gemfiles/rails_4_2.gemfile 0000644 0000041 0000041 00000000744 14042233640 021122 0 ustar www-data www-data # This file was generated by Appraisal
source "http://rubygems.org"
gem "rake"
gem "appraisal"
gem "activerecord", "~> 4.2.0"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
gem "test_after_commit", "~> 0.4.2"
end
group :sqlite do
gem "sqlite3", "~> 1.3.13"
end
group :postgresql do
gem "pg", "~> 0.18.4"
end
group :mysql do
gem "mysql2", "~> 0.4.0"
end
gemspec path: "../"
acts_as_list-1.0.4/gemfiles/rails_5_2.gemfile 0000644 0000041 0000041 00000000672 14042233640 021123 0 ustar www-data www-data # This file was generated by Appraisal
source "http://rubygems.org"
gem "rake"
gem "appraisal"
gem "activerecord", "~> 5.2.0"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
end
group :sqlite do
gem "sqlite3", "~> 1.4"
end
group :postgresql do
gem "pg", "~> 1.2.0"
end
group :mysql do
gem "mysql2", "~> 0.5.0"
end
gemspec path: "../"
acts_as_list-1.0.4/gemfiles/rails_6_1.gemfile 0000644 0000041 0000041 00000000673 14042233640 021124 0 ustar www-data www-data # This file was generated by Appraisal
source "http://rubygems.org"
gem "rake"
gem "appraisal"
gem "activerecord", "6.1.0.rc1"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
end
group :sqlite do
gem "sqlite3", "~> 1.4"
end
group :postgresql do
gem "pg", "~> 1.2.0"
end
group :mysql do
gem "mysql2", "~> 0.5.0"
end
gemspec path: "../"
acts_as_list-1.0.4/gemfiles/rails_5_0.gemfile 0000644 0000041 0000041 00000000675 14042233640 021124 0 ustar www-data www-data # This file was generated by Appraisal
source "http://rubygems.org"
gem "rake"
gem "appraisal"
gem "activerecord", "~> 5.0.0"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
end
group :sqlite do
gem "sqlite3", "~> 1.3.13"
end
group :postgresql do
gem "pg", "~> 1.2.0"
end
group :mysql do
gem "mysql2", "~> 0.5.0"
end
gemspec path: "../"
acts_as_list-1.0.4/gemfiles/rails_5_1.gemfile 0000644 0000041 0000041 00000000672 14042233640 021122 0 ustar www-data www-data # This file was generated by Appraisal
source "http://rubygems.org"
gem "rake"
gem "appraisal"
gem "activerecord", "~> 5.1.0"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
end
group :sqlite do
gem "sqlite3", "~> 1.4"
end
group :postgresql do
gem "pg", "~> 1.2.0"
end
group :mysql do
gem "mysql2", "~> 0.5.0"
end
gemspec path: "../"
acts_as_list-1.0.4/gemfiles/rails_6_0.gemfile 0000644 0000041 0000041 00000000672 14042233640 021122 0 ustar www-data www-data # This file was generated by Appraisal
source "http://rubygems.org"
gem "rake"
gem "appraisal"
gem "activerecord", "~> 6.0.0"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
end
group :sqlite do
gem "sqlite3", "~> 1.4"
end
group :postgresql do
gem "pg", "~> 1.2.0"
end
group :mysql do
gem "mysql2", "~> 0.5.0"
end
gemspec path: "../"
acts_as_list-1.0.4/acts_as_list.gemspec 0000644 0000041 0000041 00000003241 14042233640 020227 0 ustar www-data www-data # -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "acts_as_list/version"
Gem::Specification.new do |s|
# Description Meta...
s.name = "acts_as_list"
s.version = ActiveRecord::Acts::List::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Swanand Pagnis", "Brendon Muir"]
s.email = %w(swanand.pagnis@gmail.com brendon@spikeatschool.co.nz)
s.homepage = "http://github.com/brendon/acts_as_list"
s.summary = "A gem adding sorting, reordering capabilities to an active_record model, allowing it to act as a list"
s.description = 'This "acts_as" extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a "position" column defined as an integer on the mapped database table.'
s.license = "MIT"
s.required_ruby_version = ">= 2.4.7"
if s.respond_to?(:metadata)
s.metadata['changelog_uri'] = 'https://github.com/brendon/acts_as_list/blob/master/CHANGELOG.md'
s.metadata['source_code_uri'] = 'https://github.com/brendon/acts_as_list'
s.metadata['bug_tracker_uri'] = 'https://github.com/brendon/acts_as_list/issues'
end
# Load Paths...
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map {|f| File.basename(f)}
s.require_paths = ["lib"]
# Dependencies (installed via "bundle install")
s.add_dependency "activerecord", ">= 4.2"
s.add_development_dependency "bundler", ">= 1.0.0"
end
acts_as_list-1.0.4/CHANGELOG.md 0000644 0000041 0000041 00000126274 14042233640 016037 0 ustar www-data www-data # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## v1.0.4 - 2021-04-20
### Fixed
- Add Tests ruby 2.7 and 3.0 [\#393](https://github.com/brendon/acts_as_list/pull/393) ([QWYNG])
## v1.0.3 - 2020-12-24
### Fixed
- Silence deprecation warnings in Rails 6 [\#384](https://github.com/brendon/acts_as_list/pull/384) ([h-lame])
- Add explicit requirement of ActiveSupport::Inflector [\#387](https://github.com/brendon/acts_as_list/pull/387) ([rdvdijk])
## v1.0.2 - 2020-09-14
### Fixed
- Get foreign key from reflections when possible [\#383](https://github.com/brendon/acts_as_list/pull/383) ([jefftsang])
### Removed
- gemspec: Drop defunct `rubyforge_project` directive [\#373](https://github.com/brendon/acts_as_list/pull/373) ([olleolleolle])
[olleolleolle]: https://github.com/olleolleolle
[jefftsang]: https://github.com/jefftsang
## v1.0.1 - 2020-02-27
### Fixed
- Invert order when incrementing to circumvent unique index violations (#368)
## [v1.0.0](https://github.com/swanandp/acts_as_list/tree/v1.0.0) - 2019-09-26
[Full Changelog](https://github.com/swanandp/acts_as_list/compare/v0.9.19...v1.0.0)
### Removed
- **BREAKING CHANGE**: Support for Rails 3.2 > 4.1 has been removed. 0.9.19 is the last version that supports
these Rails versions
### Added
- Added *Troubleshooting Database Deadlock Errors* section to `README.md`
- Added support for Rails 6.0 in testing
- Various README fixes
- A new method called `current_position` now exists and returns the integer position of the item it's
called on, or `nil` if the position isn't set.
## [v0.9.19](https://github.com/swanandp/acts_as_list/tree/v0.9.19) - 2019-03-12
[Full Changelog](https://github.com/swanandp/acts_as_list/compare/v0.9.18...v0.9.19)
### Added
- Allow `acts_as_list_no_update` blocks to be nested [@conorbdaly](https://github.com/conorbdaly)
## [v0.9.18](https://github.com/swanandp/acts_as_list/tree/v0.9.18) - 2019-03-08
[Full Changelog](https://github.com/swanandp/acts_as_list/compare/v0.9.17...v0.9.18)
### Added
- Added additional gemspec metadata [@boone](https://github.com/boone)
- Add gem version badge to README [@joshuapinter](https://github.com/joshuapinter)
- Add touch on update configuration [@mgbatchelor](https://github.com/mgbatchelor)
### Changed
- Let's start a new direction with the CHANGELOG file [@mainameiz](https://github.com/mainameiz)
### Fixed
- Fix sqlite3 gem pinning breaking tests
## [v0.9.17](https://github.com/brendon/acts_as_list/tree/v0.9.17) (2018-10-29)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.16...v0.9.17)
**Closed issues:**
- Inconsistent behavior [\#330](https://github.com/brendon/acts_as_list/issues/330)
- Using `top\_of\_list` set to 1 and setting a record to index 0 triggers a `PG::UniqueViolation` [\#322](https://github.com/brendon/acts_as_list/issues/322)
**Merged pull requests:**
- Feature/add exception to wrong position [\#323](https://github.com/brendon/acts_as_list/pull/323) ([TheNeikos](https://github.com/TheNeikos))
- Methods move\_to\_bottom and move\_to\_top should not fail when there are unique constraints [\#320](https://github.com/brendon/acts_as_list/pull/320) ([faucct](https://github.com/faucct))
## [v0.9.16](https://github.com/brendon/acts_as_list/tree/v0.9.16) (2018-08-30)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.15...v0.9.16)
**Closed issues:**
- Re-ordering at specific position [\#318](https://github.com/brendon/acts_as_list/issues/318)
- `no\_update` is not applied to subclasses [\#314](https://github.com/brendon/acts_as_list/issues/314)
- NoMethodError: undefined method `acts\_as\_list' [\#303](https://github.com/brendon/acts_as_list/issues/303)
- Cannot create item at position 0 [\#297](https://github.com/brendon/acts_as_list/issues/297)
**Merged pull requests:**
- Unscope `select` to avoid PG::UndefinedFunction [\#283](https://github.com/brendon/acts_as_list/pull/283) ([donv](https://github.com/donv))
## [v0.9.15](https://github.com/brendon/acts_as_list/tree/v0.9.15) (2018-06-11)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.14...v0.9.15)
**Merged pull requests:**
- Fix \#314: `no\_update` is not applied to subclasses [\#315](https://github.com/brendon/acts_as_list/pull/315) ([YoranBrondsema](https://github.com/YoranBrondsema))
## [v0.9.14](https://github.com/brendon/acts_as_list/tree/v0.9.14) (2018-06-05)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.13...v0.9.14)
**Closed issues:**
- `insert\_at` saves invalid ActiveRecord objects [\#311](https://github.com/brendon/acts_as_list/issues/311)
**Merged pull requests:**
- \#311 Don't insert invalid ActiveRecord objects [\#312](https://github.com/brendon/acts_as_list/pull/312) ([seanabrahams](https://github.com/seanabrahams))
## [v0.9.13](https://github.com/brendon/acts_as_list/tree/v0.9.13) (2018-06-05)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.12...v0.9.13)
**Merged pull requests:**
- Fix unique index constraint failure on item destroy [\#313](https://github.com/brendon/acts_as_list/pull/313) ([yjukaku](https://github.com/yjukaku))
## [v0.9.12](https://github.com/brendon/acts_as_list/tree/v0.9.12) (2018-05-02)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.11...v0.9.12)
**Closed issues:**
- acts\_as\_list methods on has\_many through [\#308](https://github.com/brendon/acts_as_list/issues/308)
- Travis badge [\#307](https://github.com/brendon/acts_as_list/issues/307)
- Unscoping breaks STI subclasses, but is soon to be fixed in Rails [\#291](https://github.com/brendon/acts_as_list/issues/291)
- Refactor string eval for scope\_condition [\#227](https://github.com/brendon/acts_as_list/issues/227)
**Merged pull requests:**
- mocha/minitest, not mocha/mini\_test now. [\#310](https://github.com/brendon/acts_as_list/pull/310) ([jmarkbrooks](https://github.com/jmarkbrooks))
## [v0.9.11](https://github.com/brendon/acts_as_list/tree/v0.9.11) (2018-03-19)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.10...v0.9.11)
**Closed issues:**
- Setting `position: nil` on update returns `Column 'position' cannot be null` instead of putting the item at the start or the end of the list, like it does on create. [\#302](https://github.com/brendon/acts_as_list/issues/302)
- Switching to Semaphore [\#301](https://github.com/brendon/acts_as_list/issues/301)
- Dropping jruby support [\#300](https://github.com/brendon/acts_as_list/issues/300)
- Rails 5.2.0 [\#299](https://github.com/brendon/acts_as_list/issues/299)
- Cannot update record position when scoped to enum [\#298](https://github.com/brendon/acts_as_list/issues/298)
- `add\_new\_at: :top` does not work [\#296](https://github.com/brendon/acts_as_list/issues/296)
- remove\_from\_list causing "wrong number of arguments \(given 2, expected 0..1\)" [\#293](https://github.com/brendon/acts_as_list/issues/293)
- Passing raw strings to reorder deprecated in Rails 5.2 [\#290](https://github.com/brendon/acts_as_list/issues/290)
**Merged pull requests:**
- Fix Test Suite [\#306](https://github.com/brendon/acts_as_list/pull/306) ([brendon](https://github.com/brendon))
- Add frozen\_string\_literal pragma to ruby files [\#305](https://github.com/brendon/acts_as_list/pull/305) ([krzysiek1507](https://github.com/krzysiek1507))
- Use symbols instead of SQL strings for reorder \(for Rails 5.2\) [\#294](https://github.com/brendon/acts_as_list/pull/294) ([jhawthorn](https://github.com/jhawthorn))
## [v0.9.10](https://github.com/brendon/acts_as_list/tree/v0.9.10) (2017-11-19)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.9...v0.9.10)
**Closed issues:**
- Make insert\_at respect position when creating a new record [\#287](https://github.com/brendon/acts_as_list/issues/287)
- Why does acts\_as\_list override rails validation on it's own field? [\#269](https://github.com/brendon/acts_as_list/issues/269)
**Merged pull requests:**
- Change error classes parents [\#288](https://github.com/brendon/acts_as_list/pull/288) ([alexander-lazarov](https://github.com/alexander-lazarov))
## [v0.9.9](https://github.com/brendon/acts_as_list/tree/v0.9.9) (2017-10-03)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.8...v0.9.9)
**Merged pull requests:**
- Added fixed values option for scope array [\#286](https://github.com/brendon/acts_as_list/pull/286) ([smoyth](https://github.com/smoyth))
## [v0.9.8](https://github.com/brendon/acts_as_list/tree/v0.9.8) (2017-09-28)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.7...v0.9.8)
**Closed issues:**
- Deadlocking in update\_positions count query [\#285](https://github.com/brendon/acts_as_list/issues/285)
- Updating the position fails uniqueness constraint. [\#275](https://github.com/brendon/acts_as_list/issues/275)
## [v0.9.7](https://github.com/brendon/acts_as_list/tree/v0.9.7) (2017-07-06)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.6...v0.9.7)
## [v0.9.6](https://github.com/brendon/acts_as_list/tree/v0.9.6) (2017-07-05)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.5...v0.9.6)
**Closed issues:**
- undefined method `+' for nil:NilClass [\#278](https://github.com/brendon/acts_as_list/issues/278)
- Enum does not scope correctly [\#277](https://github.com/brendon/acts_as_list/issues/277)
- Can we don't use remove\_from\_list when destroy a lot objects? [\#276](https://github.com/brendon/acts_as_list/issues/276)
- The NoUpdate code rely's on AS::Concern [\#273](https://github.com/brendon/acts_as_list/issues/273)
- ActiveRecord associations are no longer required \(even though belongs\_to\_required\_by\_default == true\) [\#268](https://github.com/brendon/acts_as_list/issues/268)
- Unique constraint violation on move\_higher, move\_lower, destroy [\#267](https://github.com/brendon/acts_as_list/issues/267)
**Merged pull requests:**
- Fix Fixnum deprecation warnings. [\#282](https://github.com/brendon/acts_as_list/pull/282) ([patrickdavey](https://github.com/patrickdavey))
- Fix update to scope that was defined with an enum [\#281](https://github.com/brendon/acts_as_list/pull/281) ([scottmalone](https://github.com/scottmalone))
- Refactor update\_all\_with\_touch [\#279](https://github.com/brendon/acts_as_list/pull/279) ([ledestin](https://github.com/ledestin))
- Remove AS::Concern from NoUpdate [\#274](https://github.com/brendon/acts_as_list/pull/274) ([brendon](https://github.com/brendon))
- Use `ActiveSupport.on\_load` to hook into ActiveRecord [\#272](https://github.com/brendon/acts_as_list/pull/272) ([brendon](https://github.com/brendon))
## [v0.9.5](https://github.com/brendon/acts_as_list/tree/v0.9.5) (2017-04-04)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.4...v0.9.5)
**Closed issues:**
- acts\_as\_list\_class.maximum\(position\_column\) is causing the entire table to lock [\#264](https://github.com/brendon/acts_as_list/issues/264)
- Be more precise with unscope-ing [\#263](https://github.com/brendon/acts_as_list/issues/263)
**Merged pull requests:**
- Use bottom\_position\_in\_list instead of the highest value in the table [\#266](https://github.com/brendon/acts_as_list/pull/266) ([brendon](https://github.com/brendon))
- Be more surgical about unscoping [\#265](https://github.com/brendon/acts_as_list/pull/265) ([brendon](https://github.com/brendon))
## [v0.9.4](https://github.com/brendon/acts_as_list/tree/v0.9.4) (2017-03-16)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.3...v0.9.4)
**Merged pull requests:**
- Optimize first? and last? instance methods. [\#262](https://github.com/brendon/acts_as_list/pull/262) ([marshall-lee](https://github.com/marshall-lee))
## [v0.9.3](https://github.com/brendon/acts_as_list/tree/v0.9.3) (2017-03-14)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.2...v0.9.3)
**Closed issues:**
- Rails 5.1.0.beta1 deprecation [\#257](https://github.com/brendon/acts_as_list/issues/257)
- Move item X after item Y [\#256](https://github.com/brendon/acts_as_list/issues/256)
- Is there way to specify the position when creating a resource? [\#255](https://github.com/brendon/acts_as_list/issues/255)
**Merged pull requests:**
- Don't update a child destroyed via relation [\#261](https://github.com/brendon/acts_as_list/pull/261) ([brendon](https://github.com/brendon))
- No update list for collection classes [\#260](https://github.com/brendon/acts_as_list/pull/260) ([IlkhamGaysin](https://github.com/IlkhamGaysin))
- Fix deprecation introduced in ActiveRecord 5.1.0.beta1. Closes \#257 [\#259](https://github.com/brendon/acts_as_list/pull/259) ([CvX](https://github.com/CvX))
- Refactor column definer module [\#258](https://github.com/brendon/acts_as_list/pull/258) ([ledestin](https://github.com/ledestin))
## [v0.9.2](https://github.com/brendon/acts_as_list/tree/v0.9.2) (2017-02-07)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.1...v0.9.2)
**Closed issues:**
- Getting invalid input syntax for uuid [\#253](https://github.com/brendon/acts_as_list/issues/253)
## [v0.9.1](https://github.com/brendon/acts_as_list/tree/v0.9.1) (2017-01-26)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.9.0...v0.9.1)
**Closed issues:**
- DEPRECATION WARNING on rails 5.0 as of acts\_as\_list 0.9 [\#251](https://github.com/brendon/acts_as_list/issues/251)
- highter\_items returns items with the same position value [\#247](https://github.com/brendon/acts_as_list/issues/247)
- Broken with unique constraint on position [\#245](https://github.com/brendon/acts_as_list/issues/245)
**Merged pull requests:**
- fixes \#251 table\_exists? deprecation warning with Rails 5.0 [\#252](https://github.com/brendon/acts_as_list/pull/252) ([zharikovpro](https://github.com/zharikovpro))
## [v0.9.0](https://github.com/brendon/acts_as_list/tree/v0.9.0) (2017-01-23)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.8.2...v0.9.0)
**Closed issues:**
- warning: too many arguments for format string [\#239](https://github.com/brendon/acts_as_list/issues/239)
- Broken tests related to time comparison [\#238](https://github.com/brendon/acts_as_list/issues/238)
- Shuffling positions is halting the callback chain [\#234](https://github.com/brendon/acts_as_list/issues/234)
- Reorder positions [\#233](https://github.com/brendon/acts_as_list/issues/233)
- Tests break when upgrading from 0.7.2 to 0.7.4 [\#228](https://github.com/brendon/acts_as_list/issues/228)
- RE \#221 needing a test [\#226](https://github.com/brendon/acts_as_list/issues/226)
- Adding to existing model with data and methods don't work [\#209](https://github.com/brendon/acts_as_list/issues/209)
- Position is set incorrectly when circular dependencies exist [\#153](https://github.com/brendon/acts_as_list/issues/153)
**Merged pull requests:**
- Revert "Updates documentation with valid string interpolation syntax" [\#250](https://github.com/brendon/acts_as_list/pull/250) ([brendon](https://github.com/brendon))
- Updates documentation with valid string interpolation syntax [\#249](https://github.com/brendon/acts_as_list/pull/249) ([naveedkakal](https://github.com/naveedkakal))
- Comply to tests warnings [\#248](https://github.com/brendon/acts_as_list/pull/248) ([randoum](https://github.com/randoum))
- insert\_at respects unique not null check \(\>= 0\) db constraints [\#246](https://github.com/brendon/acts_as_list/pull/246) ([zharikovpro](https://github.com/zharikovpro))
- acts\_as\_list\_no\_update [\#244](https://github.com/brendon/acts_as_list/pull/244) ([randoum](https://github.com/randoum))
- Update README.md [\#243](https://github.com/brendon/acts_as_list/pull/243) ([rahuldstiwari](https://github.com/rahuldstiwari))
- Fixed tests to prevent warning: too many arguments for format string [\#242](https://github.com/brendon/acts_as_list/pull/242) ([brendon](https://github.com/brendon))
- Be explicit about ordering when mapping :pos [\#241](https://github.com/brendon/acts_as_list/pull/241) ([brendon](https://github.com/brendon))
- Improve load method [\#240](https://github.com/brendon/acts_as_list/pull/240) ([brendon](https://github.com/brendon))
- Fix non regular sequence movement [\#237](https://github.com/brendon/acts_as_list/pull/237) ([tiagotex](https://github.com/tiagotex))
- Add travis config for testing against multiple databases [\#236](https://github.com/brendon/acts_as_list/pull/236) ([fschwahn](https://github.com/fschwahn))
- Extract modules [\#229](https://github.com/brendon/acts_as_list/pull/229) ([ledestin](https://github.com/ledestin))
## [v0.8.2](https://github.com/brendon/acts_as_list/tree/v0.8.2) (2016-09-23)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.8.1...v0.8.2)
**Closed issues:**
- We're a repo now, no longer a fork attached to rails/acts\_as\_list [\#232](https://github.com/brendon/acts_as_list/issues/232)
- Break away from rails/acts\_as\_list [\#224](https://github.com/brendon/acts_as_list/issues/224)
- Problem when inserting straight at top of list [\#109](https://github.com/brendon/acts_as_list/issues/109)
**Merged pull requests:**
- Show items with same position in higher and lower items [\#231](https://github.com/brendon/acts_as_list/pull/231) ([jpalumickas](https://github.com/jpalumickas))
- fix setting position when previous position was nil [\#230](https://github.com/brendon/acts_as_list/pull/230) ([StoneFrog](https://github.com/StoneFrog))
## [v0.8.1](https://github.com/brendon/acts_as_list/tree/v0.8.1) (2016-09-06)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.8.0...v0.8.1)
**Closed issues:**
- Rubinius Intermittent testing error [\#218](https://github.com/brendon/acts_as_list/issues/218)
- ActiveRecord dependency causes rake assets:compile to fail without access to a database [\#84](https://github.com/brendon/acts_as_list/issues/84)
**Merged pull requests:**
- Refactor class\_eval with string into class\_eval with block [\#215](https://github.com/brendon/acts_as_list/pull/215) ([rdvdijk](https://github.com/rdvdijk))
## [v0.8.0](https://github.com/brendon/acts_as_list/tree/v0.8.0) (2016-08-23)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.7.7...v0.8.0)
**Closed issues:**
- Behavior with DB default seems unclear [\#219](https://github.com/brendon/acts_as_list/issues/219)
**Merged pull requests:**
- No longer a need specify additional rbx gems [\#225](https://github.com/brendon/acts_as_list/pull/225) ([brendon](https://github.com/brendon))
- Fix position when no serial positions [\#223](https://github.com/brendon/acts_as_list/pull/223) ([jpalumickas](https://github.com/jpalumickas))
- Bug: Specifying a position with add\_new\_at: :top fails to insert at that position [\#220](https://github.com/brendon/acts_as_list/pull/220) ([brendon](https://github.com/brendon))
## [v0.7.7](https://github.com/brendon/acts_as_list/tree/v0.7.7) (2016-08-18)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.7.6...v0.7.7)
**Closed issues:**
- Issue after upgrading to 0.7.5: No connection pool with id primary found. [\#214](https://github.com/brendon/acts_as_list/issues/214)
- Changing scope is inconsistent based on add\_new\_at [\#138](https://github.com/brendon/acts_as_list/issues/138)
- Duplicate positions and lost items [\#76](https://github.com/brendon/acts_as_list/issues/76)
**Merged pull requests:**
- Add quoted table names to some columns [\#221](https://github.com/brendon/acts_as_list/pull/221) ([jpalumickas](https://github.com/jpalumickas))
- Appraisals cleanup [\#217](https://github.com/brendon/acts_as_list/pull/217) ([brendon](https://github.com/brendon))
- Fix insert\_at\_position in race condition [\#195](https://github.com/brendon/acts_as_list/pull/195) ([danielross](https://github.com/danielross))
## [v0.7.6](https://github.com/brendon/acts_as_list/tree/v0.7.6) (2016-07-15)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.7.5...v0.7.6)
**Closed issues:**
- add\_new\_at nil with scope causes NoMethodError [\#211](https://github.com/brendon/acts_as_list/issues/211)
**Merged pull requests:**
- Add class method acts\_as\_list\_top as reader for configured top\_of\_list [\#213](https://github.com/brendon/acts_as_list/pull/213) ([krzysiek1507](https://github.com/krzysiek1507))
- Bugfix/add new at nil on scope change [\#212](https://github.com/brendon/acts_as_list/pull/212) ([greatghoul](https://github.com/greatghoul))
## [v0.7.5](https://github.com/brendon/acts_as_list/tree/v0.7.5) (2016-06-30)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.7.4...v0.7.5)
**Implemented enhancements:**
- Touch when reordering [\#173](https://github.com/brendon/acts_as_list/pull/173) ([botandrose](https://github.com/botandrose))
**Closed issues:**
- Exception raised when calling destroy "NameError - instance variable @scope\_changed not defined:" [\#206](https://github.com/brendon/acts_as_list/issues/206)
- Undefined instance variable @scope\_changed since 0.7.3 [\#199](https://github.com/brendon/acts_as_list/issues/199)
- Reordering large lists is slow [\#198](https://github.com/brendon/acts_as_list/issues/198)
- Reparenting child leaves gap in source list in rails 5 [\#194](https://github.com/brendon/acts_as_list/issues/194)
- Support rails 5 ? [\#186](https://github.com/brendon/acts_as_list/issues/186)
- I get a NoMethodError: undefined method `acts\_as\_list' when trying to include acts\_as\_list [\#176](https://github.com/brendon/acts_as_list/issues/176)
- Phenomenon of mysterious value of the position is skipped by one [\#166](https://github.com/brendon/acts_as_list/issues/166)
- Model.find being called twice with acts\_as\_list on destroy [\#161](https://github.com/brendon/acts_as_list/issues/161)
- `scope\_changed?` problem with acts\_as\_paranoid [\#158](https://github.com/brendon/acts_as_list/issues/158)
- Inconsistent behaviour between Symbol and Array scopes [\#155](https://github.com/brendon/acts_as_list/issues/155)
- insert\_at doesn't seem to be working in ActiveRecord callback \(Rails 4.2\) [\#150](https://github.com/brendon/acts_as_list/issues/150)
- Project Documentation link redirects to expired domain [\#149](https://github.com/brendon/acts_as_list/issues/149)
- Problem when updating an position of array of AR objects. [\#137](https://github.com/brendon/acts_as_list/issues/137)
- Unexpected behaviour when inserting consecutive items with default positions [\#124](https://github.com/brendon/acts_as_list/issues/124)
- self.reload prone to error [\#122](https://github.com/brendon/acts_as_list/issues/122)
- Rails 3.0.x in\_list causes the return of default\_scope [\#120](https://github.com/brendon/acts_as_list/issues/120)
- Relationships with dependency:destroy cause ActiveRecord::RecordNotFound [\#118](https://github.com/brendon/acts_as_list/issues/118)
- Using insert\_at with values with type String [\#117](https://github.com/brendon/acts_as_list/issues/117)
- Batch setting of position [\#112](https://github.com/brendon/acts_as_list/issues/112)
- position: 0 now makes model pushed to top? [\#110](https://github.com/brendon/acts_as_list/issues/110)
- Create element in default position [\#103](https://github.com/brendon/acts_as_list/issues/103)
- Enhancement: Expose scope object [\#97](https://github.com/brendon/acts_as_list/issues/97)
- Shuffle list [\#96](https://github.com/brendon/acts_as_list/issues/96)
- Creating an item with a nil scope should not add it to the list [\#92](https://github.com/brendon/acts_as_list/issues/92)
- Performance Improvements [\#88](https://github.com/brendon/acts_as_list/issues/88)
- has\_many :through or has\_many\_and\_belongs\_to\_many support [\#86](https://github.com/brendon/acts_as_list/issues/86)
- move\_higher/move\_lower vs move\_to\_top/move\_to\_bottom act differently when item is already at top or bottom [\#77](https://github.com/brendon/acts_as_list/issues/77)
- Limiting the list size [\#61](https://github.com/brendon/acts_as_list/issues/61)
- Adding multiple creates strange ordering [\#55](https://github.com/brendon/acts_as_list/issues/55)
- Feature: sort [\#26](https://github.com/brendon/acts_as_list/issues/26)
**Merged pull requests:**
- Fix position when no serial positions [\#208](https://github.com/brendon/acts_as_list/pull/208) ([PoslinskiNet](https://github.com/PoslinskiNet))
- Removed duplicated assignment [\#207](https://github.com/brendon/acts_as_list/pull/207) ([shunwen](https://github.com/shunwen))
- Quote all identifiers [\#205](https://github.com/brendon/acts_as_list/pull/205) ([fabn](https://github.com/fabn))
- Start testing Rails 5 [\#203](https://github.com/brendon/acts_as_list/pull/203) ([brendon](https://github.com/brendon))
- Lock! the record before destroying [\#201](https://github.com/brendon/acts_as_list/pull/201) ([brendon](https://github.com/brendon))
- Fix ambiguous column error when joining some relations [\#180](https://github.com/brendon/acts_as_list/pull/180) ([natw](https://github.com/natw))
## [v0.7.4](https://github.com/brendon/acts_as_list/tree/v0.7.4) (2016-04-15)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.7.3...v0.7.4)
**Closed issues:**
- Releasing a new gem version [\#196](https://github.com/brendon/acts_as_list/issues/196)
**Merged pull requests:**
- Fix scope changed [\#200](https://github.com/brendon/acts_as_list/pull/200) ([brendon](https://github.com/brendon))
## [v0.7.3](https://github.com/brendon/acts_as_list/tree/v0.7.3) (2016-04-14)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/v0.7.2...v0.7.3)
## [v0.7.2](https://github.com/brendon/acts_as_list/tree/v0.7.2) (2016-04-01)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.7.2...v0.7.2)
**Closed issues:**
- DEPRECATION WARNING: Passing string to define callback on Rails 5 beta 3 [\#191](https://github.com/brendon/acts_as_list/issues/191)
- Why is `add\_to\_list\_bottom` private? [\#187](https://github.com/brendon/acts_as_list/issues/187)
- Ordering of children when there are two possible parent models. [\#172](https://github.com/brendon/acts_as_list/issues/172)
- Fix the jruby and rbx builds [\#169](https://github.com/brendon/acts_as_list/issues/169)
- Unable to run tests [\#162](https://github.com/brendon/acts_as_list/issues/162)
- shuffle\_positions\_on\_intermediate\_items is creating problems [\#134](https://github.com/brendon/acts_as_list/issues/134)
- introduce Changelog file to quickly track changes [\#68](https://github.com/brendon/acts_as_list/issues/68)
- Mongoid support? [\#52](https://github.com/brendon/acts_as_list/issues/52)
**Merged pull requests:**
- Add filename/line number to class\_eval call [\#193](https://github.com/brendon/acts_as_list/pull/193) ([hfwang](https://github.com/hfwang))
- Use a symbol as a string to define callback [\#192](https://github.com/brendon/acts_as_list/pull/192) ([brendon](https://github.com/brendon))
- Pin changelog generator to a working version [\#190](https://github.com/brendon/acts_as_list/pull/190) ([fabn](https://github.com/fabn))
- Fix bug, position is recomputed when object saved [\#188](https://github.com/brendon/acts_as_list/pull/188) ([chrisortman](https://github.com/chrisortman))
- Update bundler before running tests, fixes test run on travis [\#179](https://github.com/brendon/acts_as_list/pull/179) ([fabn](https://github.com/fabn))
- Changelog generator, closes \#68 [\#177](https://github.com/brendon/acts_as_list/pull/177) ([fabn](https://github.com/fabn))
- Updating README example [\#175](https://github.com/brendon/acts_as_list/pull/175) ([ryanbillings](https://github.com/ryanbillings))
- Adds description about various options available with the acts\_as\_list method [\#168](https://github.com/brendon/acts_as_list/pull/168) ([udit7590](https://github.com/udit7590))
- Small changes to DRY up list.rb [\#163](https://github.com/brendon/acts_as_list/pull/163) ([Albin-Willman](https://github.com/Albin-Willman))
- Only swap changed attributes which are persistable, i.e. are DB columns. [\#152](https://github.com/brendon/acts_as_list/pull/152) ([ludwigschubert](https://github.com/ludwigschubert))
## [0.7.2](https://github.com/brendon/acts_as_list/tree/0.7.2) (2015-05-06)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.7.1...0.7.2)
## [0.7.1](https://github.com/brendon/acts_as_list/tree/0.7.1) (2015-05-06)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.7.0...0.7.1)
**Merged pull requests:**
- Update README.md [\#159](https://github.com/brendon/acts_as_list/pull/159) ([tibastral](https://github.com/tibastral))
## [0.7.0](https://github.com/brendon/acts_as_list/tree/0.7.0) (2015-05-01)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.6.0...0.7.0)
**Closed issues:**
- Problem with reordering scoped list items [\#154](https://github.com/brendon/acts_as_list/issues/154)
- Can no longer load acts\_as\_list in isolation if Rails is installed [\#145](https://github.com/brendon/acts_as_list/issues/145)
**Merged pull requests:**
- Fix regression with using acts\_as\_list on base classes [\#147](https://github.com/brendon/acts_as_list/pull/147) ([botandrose](https://github.com/botandrose))
- Don't require rails when loading [\#146](https://github.com/brendon/acts_as_list/pull/146) ([botandrose](https://github.com/botandrose))
## [0.6.0](https://github.com/brendon/acts_as_list/tree/0.6.0) (2014-12-24)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.5.0...0.6.0)
**Closed issues:**
- Deprecation Warning: sanitize\_sql\_hash\_for\_conditions is deprecated and will be removed in Rails 5.0 [\#143](https://github.com/brendon/acts_as_list/issues/143)
- Release a new gem version [\#136](https://github.com/brendon/acts_as_list/issues/136)
**Merged pull requests:**
- Fix sanitize\_sql\_hash\_for\_conditions deprecation warning in Rails 4.2 [\#140](https://github.com/brendon/acts_as_list/pull/140) ([eagletmt](https://github.com/eagletmt))
- Simpler method to find the subclass name [\#139](https://github.com/brendon/acts_as_list/pull/139) ([brendon](https://github.com/brendon))
- Rails4 enum column support [\#130](https://github.com/brendon/acts_as_list/pull/130) ([arunagw](https://github.com/arunagw))
- use eval for determing the self.class.name useful when this is used in an abstract class [\#123](https://github.com/brendon/acts_as_list/pull/123) ([flarik](https://github.com/flarik))
## [0.5.0](https://github.com/brendon/acts_as_list/tree/0.5.0) (2014-10-31)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.4.0...0.5.0)
**Closed issues:**
- I want to have my existing records works like list [\#133](https://github.com/brendon/acts_as_list/issues/133)
- Add Support For Multiple Indexes [\#127](https://github.com/brendon/acts_as_list/issues/127)
- changing parent\_id does not update item positions [\#126](https://github.com/brendon/acts_as_list/issues/126)
- How to exclude objects to be positioned? [\#125](https://github.com/brendon/acts_as_list/issues/125)
- Scope for Polymorphic association + ManyToMany [\#106](https://github.com/brendon/acts_as_list/issues/106)
- Bug when use \#insert\_at on an invalid ActiveRecord object [\#99](https://github.com/brendon/acts_as_list/issues/99)
- has\_many :through with acts as list [\#95](https://github.com/brendon/acts_as_list/issues/95)
- Update position when scope changes [\#19](https://github.com/brendon/acts_as_list/issues/19)
**Merged pull requests:**
- Cast column default value to int before comparing with position column [\#129](https://github.com/brendon/acts_as_list/pull/129) ([wioux](https://github.com/wioux))
- Fix travis builds for rbx [\#128](https://github.com/brendon/acts_as_list/pull/128) ([meineerde](https://github.com/meineerde))
- Use unscoped blocks instead of chaining [\#121](https://github.com/brendon/acts_as_list/pull/121) ([brendon](https://github.com/brendon))
- Make acts\_as\_list more compatible with BINARY column [\#116](https://github.com/brendon/acts_as_list/pull/116) ([sikachu](https://github.com/sikachu))
- Added help notes on non-association scopes [\#115](https://github.com/brendon/acts_as_list/pull/115) ([VorontsovIE](https://github.com/VorontsovIE))
- Let AR::Base properly lazy-loaded if Railtie is available [\#114](https://github.com/brendon/acts_as_list/pull/114) ([amatsuda](https://github.com/amatsuda))
## [0.4.0](https://github.com/brendon/acts_as_list/tree/0.4.0) (2014-02-22)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.3.0...0.4.0)
**Closed issues:**
- insert\_at creates gaps [\#108](https://github.com/brendon/acts_as_list/issues/108)
- move\_lower and move\_higher not working returning nil [\#57](https://github.com/brendon/acts_as_list/issues/57)
- Mass-assignment issue with 0.1.8 [\#50](https://github.com/brendon/acts_as_list/issues/50)
- validates error [\#49](https://github.com/brendon/acts_as_list/issues/49)
- Ability to move multiple at once [\#40](https://github.com/brendon/acts_as_list/issues/40)
- Duplicates created when using accepts\_nested\_attributes\_for [\#29](https://github.com/brendon/acts_as_list/issues/29)
**Merged pull requests:**
- Update README [\#107](https://github.com/brendon/acts_as_list/pull/107) ([Senjai](https://github.com/Senjai))
- Add license info: license file and gemspec [\#105](https://github.com/brendon/acts_as_list/pull/105) ([chulkilee](https://github.com/chulkilee))
- Fix top position when position is lower than top position [\#104](https://github.com/brendon/acts_as_list/pull/104) ([csaura](https://github.com/csaura))
- Get specs running under Rails 4.1.0.beta1 [\#101](https://github.com/brendon/acts_as_list/pull/101) ([petergoldstein](https://github.com/petergoldstein))
- Add support for JRuby and Rubinius specs [\#100](https://github.com/brendon/acts_as_list/pull/100) ([petergoldstein](https://github.com/petergoldstein))
- Use the correct syntax for conditions in Rails 4 on the readme. [\#94](https://github.com/brendon/acts_as_list/pull/94) ([gotjosh](https://github.com/gotjosh))
- Adds `required\_ruby\_version` to gemspec [\#90](https://github.com/brendon/acts_as_list/pull/90) ([tvdeyen](https://github.com/tvdeyen))
## [0.3.0](https://github.com/brendon/acts_as_list/tree/0.3.0) (2013-08-02)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.2.0...0.3.0)
**Closed issues:**
- act\_as\_list didn't install with bundle install [\#83](https://github.com/brendon/acts_as_list/issues/83)
- Cannot update to version 0.1.7 [\#48](https://github.com/brendon/acts_as_list/issues/48)
- when position is null all new items get inserted in position 1 [\#41](https://github.com/brendon/acts_as_list/issues/41)
**Merged pull requests:**
- Test against activerecord v3 and v4 [\#82](https://github.com/brendon/acts_as_list/pull/82) ([sanemat](https://github.com/sanemat))
- Fix check\_scope to work on lists with array scopes [\#81](https://github.com/brendon/acts_as_list/pull/81) ([conzett](https://github.com/conzett))
- Rails4 compatibility [\#80](https://github.com/brendon/acts_as_list/pull/80) ([philippfranke](https://github.com/philippfranke))
- Add tests for moving within scope and add method: move\_within\_scope [\#79](https://github.com/brendon/acts_as_list/pull/79) ([philippfranke](https://github.com/philippfranke))
- Option to not automatically add items to the list [\#72](https://github.com/brendon/acts_as_list/pull/72) ([forrest](https://github.com/forrest))
## [0.2.0](https://github.com/brendon/acts_as_list/tree/0.2.0) (2013-02-28)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.9...0.2.0)
**Merged pull requests:**
- Fix update\_all deprecation warnings in Rails 4.0.0.beta1 [\#73](https://github.com/brendon/acts_as_list/pull/73) ([soffes](https://github.com/soffes))
- Add quotes to Id in SQL requests [\#69](https://github.com/brendon/acts_as_list/pull/69) ([noefroidevaux](https://github.com/noefroidevaux))
- Update position when scope changes [\#67](https://github.com/brendon/acts_as_list/pull/67) ([philippfranke](https://github.com/philippfranke))
- add and categorize public instance methods in readme; add misc notes to ... [\#66](https://github.com/brendon/acts_as_list/pull/66) ([barelyknown](https://github.com/barelyknown))
- Updates \#bottom\_item .find syntax to \>= Rails 3 compatible syntax. [\#65](https://github.com/brendon/acts_as_list/pull/65) ([tvdeyen](https://github.com/tvdeyen))
- add GitHub Flavored Markdown to README [\#63](https://github.com/brendon/acts_as_list/pull/63) ([phlipper](https://github.com/phlipper))
## [0.1.9](https://github.com/brendon/acts_as_list/tree/0.1.9) (2012-12-04)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.8...0.1.9)
**Closed issues:**
- Mysql2 error [\#54](https://github.com/brendon/acts_as_list/issues/54)
- Use alternative column name? [\#53](https://github.com/brendon/acts_as_list/issues/53)
**Merged pull requests:**
- attr-accessible can be damaging, is not always necessary. [\#60](https://github.com/brendon/acts_as_list/pull/60) ([graemeworthy](https://github.com/graemeworthy))
- More reliable lower/higher item detection [\#59](https://github.com/brendon/acts_as_list/pull/59) ([miks](https://github.com/miks))
- Instructions for using an array with scope [\#58](https://github.com/brendon/acts_as_list/pull/58) ([zukowski](https://github.com/zukowski))
- Attr accessible patch, should solve \#50 [\#51](https://github.com/brendon/acts_as_list/pull/51) ([fabn](https://github.com/fabn))
- support accepts\_nested\_attributes\_for multi-destroy [\#46](https://github.com/brendon/acts_as_list/pull/46) ([saberma](https://github.com/saberma))
## [0.1.8](https://github.com/brendon/acts_as_list/tree/0.1.8) (2012-08-09)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.7...0.1.8)
## [0.1.7](https://github.com/brendon/acts_as_list/tree/0.1.7) (2012-08-09)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.6...0.1.7)
**Closed issues:**
- Remove use of update\_attribute [\#44](https://github.com/brendon/acts_as_list/issues/44)
- Order is reversed when adding multiple rows at once [\#34](https://github.com/brendon/acts_as_list/issues/34)
**Merged pull requests:**
- Fixed issue with update\_positions that wasn't taking 'scope\_condition' into account [\#47](https://github.com/brendon/acts_as_list/pull/47) ([bastien](https://github.com/bastien))
- Replaced usage of update\_attribute with update\_attribute! [\#45](https://github.com/brendon/acts_as_list/pull/45) ([kevmoo](https://github.com/kevmoo))
- use self.class.primary\_key instead of id in shuffle\_positions\_on\_intermediate\_items [\#42](https://github.com/brendon/acts_as_list/pull/42) ([servercrunch](https://github.com/servercrunch))
- initialize gem [\#39](https://github.com/brendon/acts_as_list/pull/39) ([megatux](https://github.com/megatux))
- Added ability to set item positions directly \(e.g. In a form\) [\#38](https://github.com/brendon/acts_as_list/pull/38) ([dubroe](https://github.com/dubroe))
- Prevent SQL error when position\_column is not unique [\#37](https://github.com/brendon/acts_as_list/pull/37) ([hinrik](https://github.com/hinrik))
- Add installation instructions to README.md [\#35](https://github.com/brendon/acts_as_list/pull/35) ([mark-rushakoff](https://github.com/mark-rushakoff))
## [0.1.6](https://github.com/brendon/acts_as_list/tree/0.1.6) (2012-04-19)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.5...0.1.6)
**Closed issues:**
- eval mistakenly resolved the module path [\#32](https://github.com/brendon/acts_as_list/issues/32)
- Duplicated positions when creating parent and children from scratch in 0.1.5 [\#31](https://github.com/brendon/acts_as_list/issues/31)
- add info about v0.1.5 require Rails 3 [\#28](https://github.com/brendon/acts_as_list/issues/28)
- position not updated with move\_higher or move\_lover [\#23](https://github.com/brendon/acts_as_list/issues/23)
**Merged pull requests:**
- update ActiveRecord class eval to support ActiveSupport on\_load [\#33](https://github.com/brendon/acts_as_list/pull/33) ([mergulhao](https://github.com/mergulhao))
- Add :add\_new\_at option [\#30](https://github.com/brendon/acts_as_list/pull/30) ([mjbellantoni](https://github.com/mjbellantoni))
## [0.1.5](https://github.com/brendon/acts_as_list/tree/0.1.5) (2012-02-24)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.4...0.1.5)
**Closed issues:**
- increment\_positions\_on\_lower\_items called twice on insert\_at with new item [\#21](https://github.com/brendon/acts_as_list/issues/21)
- Change bundler dependency from ~\>1.0.0 to ~\>1.0 [\#20](https://github.com/brendon/acts_as_list/issues/20)
- decrement\_positions\_on\_lower\_items method [\#17](https://github.com/brendon/acts_as_list/issues/17)
- New gem release [\#16](https://github.com/brendon/acts_as_list/issues/16)
- acts\_as\_list :scope =\> "doesnt\_seem\_to\_work" [\#12](https://github.com/brendon/acts_as_list/issues/12)
- don't work perfectly with default\_scope [\#11](https://github.com/brendon/acts_as_list/issues/11)
- MySQL: Position column MUST NOT have default [\#10](https://github.com/brendon/acts_as_list/issues/10)
- insert\_at fails on postgresql w/ non-null constraint on postion\_column [\#8](https://github.com/brendon/acts_as_list/issues/8)
**Merged pull requests:**
- Efficiency improvement for insert\_at when repositioning an existing item [\#27](https://github.com/brendon/acts_as_list/pull/27) ([bradediger](https://github.com/bradediger))
- Use before validate instead of before create [\#25](https://github.com/brendon/acts_as_list/pull/25) ([webervin](https://github.com/webervin))
- Massive test refactorings. [\#24](https://github.com/brendon/acts_as_list/pull/24) ([splattael](https://github.com/splattael))
- Silent migrations to reduce test noise. [\#22](https://github.com/brendon/acts_as_list/pull/22) ([splattael](https://github.com/splattael))
- Should decrement lower items after the item has been destroyed to avoid unique key conflicts. [\#18](https://github.com/brendon/acts_as_list/pull/18) ([aepstein](https://github.com/aepstein))
- Fix spelling and grammer [\#15](https://github.com/brendon/acts_as_list/pull/15) ([tmiller](https://github.com/tmiller))
- store\_at\_0 should yank item from the list then decrement items to avoid r [\#14](https://github.com/brendon/acts_as_list/pull/14) ([aepstein](https://github.com/aepstein))
- Support default\_scope ordering by calling .unscoped [\#13](https://github.com/brendon/acts_as_list/pull/13) ([tanordheim](https://github.com/tanordheim))
## [0.1.4](https://github.com/brendon/acts_as_list/tree/0.1.4) (2011-07-27)
[Full Changelog](https://github.com/brendon/acts_as_list/compare/0.1.3...0.1.4)
**Merged pull requests:**
- Fix sqlite3 dependency [\#7](https://github.com/brendon/acts_as_list/pull/7) ([joneslee85](https://github.com/joneslee85))
## [0.1.3](https://github.com/brendon/acts_as_list/tree/0.1.3) (2011-06-10)
**Closed issues:**
- Graph like behaviour [\#5](https://github.com/brendon/acts_as_list/issues/5)
- Updated Gem? [\#4](https://github.com/brendon/acts_as_list/issues/4)
**Merged pull requests:**
- Converted into a gem... plus some slight refactors [\#6](https://github.com/brendon/acts_as_list/pull/6) ([chaffeqa](https://github.com/chaffeqa))
- Fixed test issue for test\_injection: expected SQL was reversed. [\#3](https://github.com/brendon/acts_as_list/pull/3) ([afriqs](https://github.com/afriqs))
- Added an option to set the top of the position [\#2](https://github.com/brendon/acts_as_list/pull/2) ([danielcooper](https://github.com/danielcooper))
- minor change to acts\_as\_list's callbacks [\#1](https://github.com/brendon/acts_as_list/pull/1) ([tiegz](https://github.com/tiegz))
acts_as_list-1.0.4/Appraisals 0000644 0000041 0000041 00000001221 14042233640 016230 0 ustar www-data www-data appraise "rails-4-2" do
group :mysql do
gem "mysql2", "~> 0.4.0"
end
group :postgresql do
gem "pg", "~> 0.18.4"
end
group :test do
gem "test_after_commit", "~> 0.4.2"
end
group :sqlite do
gem "sqlite3", "~> 1.3.13"
end
gem "activerecord", "~> 4.2.0"
end
appraise "rails-5-0" do
group :sqlite do
gem "sqlite3", "~> 1.3.13"
end
gem "activerecord", "~> 5.0.0"
end
appraise "rails-5-1" do
gem "activerecord", "~> 5.1.0"
end
appraise "rails-5-2" do
gem "activerecord", "~> 5.2.0"
end
appraise "rails-6-0" do
gem "activerecord", "~> 6.0.0"
end
appraise "rails-6-1" do
gem "activerecord", "6.1.0.rc1"
end
acts_as_list-1.0.4/init.rb 0000644 0000041 0000041 00000000141 14042233640 015476 0 ustar www-data www-data # frozen_string_literal: true
$:.unshift "#{File.dirname(__FILE__)}/lib"
require "acts_as_list"
acts_as_list-1.0.4/.gitignore 0000644 0000041 0000041 00000000224 14042233640 016200 0 ustar www-data www-data *.gem
.bundle
Gemfile.lock
pkg/*
.rvmrc
*.tmproj
.rbenv-version
.ruby-gemset
.ruby-version
# Appraisal generated lockfiles
*.gemfile.lock
.DS_Store
acts_as_list-1.0.4/.gemtest 0000644 0000041 0000041 00000000000 14042233640 015651 0 ustar www-data www-data acts_as_list-1.0.4/Rakefile 0000644 0000041 0000041 00000002255 14042233640 015663 0 ustar www-data www-data require "rubygems"
require "bundler/setup"
Bundler::GemHelper.install_tasks
require "rake/testtask"
# Run the test with "rake" or "rake test"
desc "Default: run acts_as_list unit tests."
task default: :test
desc "Test the acts_as_list plugin."
Rake::TestTask.new(:test) do |t|
t.libs << "test" << "."
t.test_files = Rake::FileList["test/**/test_*.rb"]
t.verbose = false
end
begin
# Run the rdoc task to generate rdocs for this gem
require "rdoc/task"
RDoc::Task.new do |rdoc|
require "acts_as_list/version"
version = ActiveRecord::Acts::List::VERSION
rdoc.rdoc_dir = "rdoc"
rdoc.title = "acts_as_list #{version}"
rdoc.rdoc_files.include("README*")
rdoc.rdoc_files.include("lib/**/*.rb")
end
rescue LoadError
puts "RDocTask is not supported on this platform."
rescue StandardError
puts "RDocTask is not supported on this platform."
end
# See https://github.com/skywinder/github-changelog-generator#rake-task for details
# and github_changelog_generator --help for available options
require 'github_changelog_generator/task'
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
config.project = 'acts_as_list'
config.user = 'brendon'
end
acts_as_list-1.0.4/lib/ 0000755 0000041 0000041 00000000000 14042233640 014760 5 ustar www-data www-data acts_as_list-1.0.4/lib/acts_as_list.rb 0000644 0000041 0000041 00000001217 14042233640 017756 0 ustar www-data www-data # frozen_string_literal: true
require "acts_as_list/active_record/acts/list"
require "acts_as_list/active_record/acts/position_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"
require "acts_as_list/active_record/acts/sequential_updates_method_definer"
require "acts_as_list/active_record/acts/active_record"
acts_as_list-1.0.4/lib/acts_as_list/ 0000755 0000041 0000041 00000000000 14042233640 017430 5 ustar www-data www-data acts_as_list-1.0.4/lib/acts_as_list/version.rb 0000644 0000041 0000041 00000000173 14042233640 021443 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord
module Acts
module List
VERSION = '1.0.4'
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/ 0000755 0000041 0000041 00000000000 14042233640 022241 5 ustar www-data www-data acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/ 0000755 0000041 0000041 00000000000 14042233640 023173 5 ustar www-data www-data acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/list.rb 0000644 0000041 0000041 00000045536 14042233640 024510 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord
module Acts #:nodoc:
module List #:nodoc:
module ClassMethods
# Configuration options are:
#
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: acts_as_list scope: 'todo_list_id = #{todo_list_id} AND completed = 0'
# * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
# act more like an array in its indexing.
# * +add_new_at+ - specifies whether objects get added to the :top or :bottom of the list. (default: +bottom+)
# `nil` will result in new items not being added to the list on create.
# * +sequential_updates+ - specifies whether insert_at should update objects positions during shuffling
# one by one to respect position column unique not null constraint.
# Defaults to true if position column has unique index, otherwise false.
# If constraint is deferrable initially deferred, overriding it with false will speed up insert_at.
# * +touch_on_update+ - configuration to disable the update of the model timestamps when the positions are updated.
def acts_as_list(options = {})
configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom, touch_on_update: true }
configuration.update(options) if options.is_a?(Hash)
caller_class = self
ActiveRecord::Acts::List::PositionColumnMethodDefiner.call(caller_class, configuration[:column], configuration[:touch_on_update])
ActiveRecord::Acts::List::ScopeMethodDefiner.call(caller_class, configuration[:scope])
ActiveRecord::Acts::List::TopOfListMethodDefiner.call(caller_class, configuration[:top_of_list])
ActiveRecord::Acts::List::AddNewAtMethodDefiner.call(caller_class, configuration[:add_new_at])
ActiveRecord::Acts::List::AuxMethodDefiner.call(caller_class)
ActiveRecord::Acts::List::CallbackDefiner.call(caller_class, configuration[:add_new_at])
ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner.call(caller_class, configuration[:column], configuration[:sequential_updates])
include ActiveRecord::Acts::List::InstanceMethods
include ActiveRecord::Acts::List::NoUpdate
end
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
# The class that has this specified needs to have a +position+ column defined as an integer on
# the mapped database table.
#
# Todo list example:
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, order: "position"
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list scope: :todo_list
# end
#
# todo_list.first.move_to_bottom
# todo_list.last.move_higher
# All the methods available to a record that has had acts_as_list specified. Each method works
# by assuming the object to be the item in the list, so chapter.move_lower would move that chapter
# lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is
# the first in the list of all chapters.
end
module InstanceMethods
# Get the current position of the item in the list
def current_position
position = send(position_column)
position ? position.to_i : nil
end
# Insert the item at the given position (defaults to the top position of 1).
def insert_at(position = acts_as_list_top)
insert_at_position(position)
end
def insert_at!(position = acts_as_list_top)
insert_at_position(position, true)
end
# Swap positions with the next lower item, if one exists.
def move_lower
return unless lower_item
acts_as_list_class.transaction do
if lower_item.current_position != current_position
swap_positions_with(lower_item)
else
lower_item.decrement_position
increment_position
end
end
end
# Swap positions with the next higher item, if one exists.
def move_higher
return unless higher_item
acts_as_list_class.transaction do
if higher_item.current_position != current_position
swap_positions_with(higher_item)
else
higher_item.increment_position
decrement_position
end
end
end
# Move to the bottom of the list. If the item is already in the list, the items below it have their
# position adjusted accordingly.
def move_to_bottom
return unless in_list?
insert_at_position bottom_position_in_list.to_i
end
# Move to the top of the list. If the item is already in the list, the items above it have their
# position adjusted accordingly.
def move_to_top
return unless in_list?
insert_at_position acts_as_list_top
end
# Removes the item from the list.
def remove_from_list
if in_list?
decrement_positions_on_lower_items
set_list_position(nil)
end
end
# Move the item within scope. If a position within the new scope isn't supplied, the item will
# be appended to the end of the list.
def move_within_scope(scope_id)
send("#{scope_name}=", scope_id)
save!
end
# Increase the position of this item without adjusting the rest of the list.
def increment_position
return unless in_list?
set_list_position(current_position + 1)
end
# Decrease the position of this item without adjusting the rest of the list.
def decrement_position
return unless in_list?
set_list_position(current_position - 1)
end
def first?
return false unless in_list?
!higher_items(1).exists?
end
def last?
return false unless in_list?
!lower_items(1).exists?
end
# Return the next higher item in the list.
def higher_item
return nil unless in_list?
higher_items(1).first
end
# Return the next n higher items in the list
# selects all higher items by default
def higher_items(limit=nil)
limit ||= acts_as_list_list.count
acts_as_list_list.
where("#{quoted_position_column_with_table_name} <= ?", current_position).
where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
reorder(acts_as_list_order_argument(:desc)).
limit(limit)
end
# Return the next lower item in the list.
def lower_item
return nil unless in_list?
lower_items(1).first
end
# Return the next n lower items in the list
# selects all lower items by default
def lower_items(limit=nil)
limit ||= acts_as_list_list.count
acts_as_list_list.
where("#{quoted_position_column_with_table_name} >= ?", current_position).
where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
reorder(acts_as_list_order_argument(:asc)).
limit(limit)
end
# Test if this record is in a list
def in_list?
!not_in_list?
end
def not_in_list?
current_position.nil?
end
def default_position
acts_as_list_class.column_defaults[position_column.to_s]
end
def default_position?
default_position && default_position == current_position
end
# Sets the new position and saves it
def set_list_position(new_position, raise_exception_if_save_fails=false)
self[position_column] = new_position
raise_exception_if_save_fails ? save! : save
end
private
def swap_positions_with(item)
item_position = item.current_position
item.set_list_position(current_position)
set_list_position(item_position)
end
def acts_as_list_list
acts_as_list_class.default_scoped.unscope(:select, :where).where(scope_condition)
end
# Poorly named methods. They will insert the item at the desired position if the position
# has been set manually using position=, not necessarily the top or bottom of the list:
def add_to_list_top
if assume_default_position?
increment_positions_on_all_items
self[position_column] = acts_as_list_top
else
increment_positions_on_lower_items(self[position_column], id)
end
# Make sure we know that we've processed this scope change already
@scope_changed = false
# Don't halt the callback chain
true
end
def add_to_list_bottom
if assume_default_position?
self[position_column] = bottom_position_in_list.to_i + 1
else
increment_positions_on_lower_items(self[position_column], id)
end
# Make sure we know that we've processed this scope change already
@scope_changed = false
# Don't halt the callback chain
true
end
def assume_default_position?
not_in_list? ||
persisted? && internal_scope_changed? && !position_changed ||
default_position?
end
# Overwrite this method to define the scope of the list changes
def scope_condition() {} end
# Returns the bottom position number in the list.
# bottom_position_in_list # => 2
def bottom_position_in_list(except = nil)
item = bottom_item(except)
item ? item.current_position : acts_as_list_top - 1
end
# Returns the bottom item
def bottom_item(except = nil)
scope = acts_as_list_list
if except
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", except.id)
end
scope.in_list.reorder(acts_as_list_order_argument(:desc)).first
end
# Forces item to assume the bottom position in the list.
def assume_bottom_position
set_list_position(bottom_position_in_list(self).to_i + 1)
end
# Forces item to assume the top position in the list.
def assume_top_position
set_list_position(acts_as_list_top)
end
# This has the effect of moving all the higher items down one.
def increment_positions_on_higher_items
return unless in_list?
acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", current_position).increment_all
end
# This has the effect of moving all the lower items down one.
def increment_positions_on_lower_items(position, avoid_id = nil)
scope = acts_as_list_list
if avoid_id
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
end
if sequential_updates?
scope.where("#{quoted_position_column_with_table_name} >= ?", position).reorder(acts_as_list_order_argument(:desc)).increment_sequentially
else
scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
end
end
# This has the effect of moving all the higher items up one.
def decrement_positions_on_higher_items(position)
acts_as_list_list.where("#{quoted_position_column_with_table_name} <= ?", position).decrement_all
end
# This has the effect of moving all the lower items up one.
def decrement_positions_on_lower_items(position=current_position)
return unless in_list?
if sequential_updates?
acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
else
acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
end
end
# Increments position (position_column) of all items in the list.
def increment_positions_on_all_items
acts_as_list_list.increment_all
end
# Reorders intermediate items to support moving an item from old_position to new_position.
# unique constraint prevents regular increment_all and forces to do increments one by one
# http://stackoverflow.com/questions/7703196/sqlite-increment-unique-integer-field
# both SQLite and PostgreSQL (and most probably MySQL too) has same issue
# that's why *sequential_updates?* check alters implementation behavior
def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
return if old_position == new_position
scope = acts_as_list_list
if avoid_id
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
end
if old_position < new_position
# Decrement position of intermediate items
#
# e.g., if moving an item from 2 to 5,
# move [3, 4, 5] to [2, 3, 4]
items = scope.where(
"#{quoted_position_column_with_table_name} > ?", old_position
).where(
"#{quoted_position_column_with_table_name} <= ?", new_position
)
if sequential_updates?
items.reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
else
items.decrement_all
end
else
# Increment position of intermediate items
#
# e.g., if moving an item from 5 to 2,
# move [2, 3, 4] to [3, 4, 5]
items = scope.where(
"#{quoted_position_column_with_table_name} >= ?", new_position
).where(
"#{quoted_position_column_with_table_name} < ?", old_position
)
if sequential_updates?
items.reorder(acts_as_list_order_argument(:desc)).increment_sequentially
else
items.increment_all
end
end
end
def insert_at_position(position, raise_exception_if_save_fails=false)
raise ArgumentError.new("position cannot be lower than top") if position < acts_as_list_top
return set_list_position(position, raise_exception_if_save_fails) if new_record?
with_lock do
if in_list?
old_position = current_position
return if position == old_position
# temporary move after bottom with gap, avoiding duplicate values
# gap is required to leave room for position increments
# positive number will be valid with unique not null check (>= 0) db constraint
temporary_position = bottom_position_in_list + 2
set_list_position(temporary_position, raise_exception_if_save_fails)
shuffle_positions_on_intermediate_items(old_position, position, id)
else
increment_positions_on_lower_items(position)
end
set_list_position(position, raise_exception_if_save_fails)
end
end
def update_positions
return unless position_before_save_changed?
old_position = position_before_save || bottom_position_in_list + 1
return unless current_position && acts_as_list_list.where(
"#{quoted_position_column_with_table_name} = #{current_position}"
).count > 1
shuffle_positions_on_intermediate_items old_position, current_position, id
end
def position_before_save_changed?
if active_record_version_is?('>= 5.1')
saved_change_to_attribute? position_column
else
attribute_changed? position_column
end
end
def position_before_save
if active_record_version_is?('>= 5.1')
attribute_before_last_save position_column
else
attribute_was position_column
end
end
def internal_scope_changed?
return @scope_changed if defined?(@scope_changed)
@scope_changed = scope_changed?
end
def clear_scope_changed
remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
end
def check_scope
if internal_scope_changed?
cached_changes = changes
cached_changes.each { |attribute, values| send("#{attribute}=", values[0]) }
send('decrement_positions_on_lower_items') if lower_item
cached_changes.each { |attribute, values| send("#{attribute}=", values[1]) }
send("add_to_list_#{add_new_at}") if add_new_at.present?
end
end
# This check is skipped if the position is currently the default position from the table
# as modifying the default position on creation is handled elsewhere
def check_top_position
if current_position && !default_position? && current_position < acts_as_list_top
self[position_column] = acts_as_list_top
end
end
# When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)
def quoted_position_column
@_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
end
# Used in order clauses
def quoted_table_name
@_quoted_table_name ||= acts_as_list_class.quoted_table_name
end
def quoted_position_column_with_table_name
@_quoted_position_column_with_table_name ||= "#{quoted_table_name}.#{quoted_position_column}"
end
def acts_as_list_order_argument(direction = :asc)
{ position_column => direction }
end
def active_record_version_is?(version_requirement)
requirement = Gem::Requirement.new(version_requirement)
version = Gem.loaded_specs['activerecord'].version
requirement.satisfied_by?(version)
end
end
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/active_record.rb 0000644 0000041 0000041 00000000173 14042233640 026332 0 ustar www-data www-data # frozen_string_literal: true
ActiveSupport.on_load :active_record do
extend ActiveRecord::Acts::List::ClassMethods
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/aux_method_definer.rb 0000644 0000041 0000041 00000000363 14042233640 027353 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord::Acts::List::AuxMethodDefiner #:nodoc:
def self.call(caller_class)
caller_class.class_eval do
define_method :acts_as_list_class do
caller_class
end
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/position_column_method_definer.rb 0000644 0000041 0000041 00000006035 14042233640 032001 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord::Acts::List::PositionColumnMethodDefiner #:nodoc:
def self.call(caller_class, position_column, touch_on_update)
define_class_methods(caller_class, position_column, touch_on_update)
define_instance_methods(caller_class, position_column)
if mass_assignment_protection_was_used_by_user?(caller_class)
protect_attributes_from_mass_assignment(caller_class, position_column)
end
end
private
def self.define_class_methods(caller_class, position_column, touch_on_update)
caller_class.class_eval do
define_singleton_method :quoted_position_column do
@_quoted_position_column ||= connection.quote_column_name(position_column)
end
define_singleton_method :quoted_position_column_with_table_name do
@_quoted_position_column_with_table_name ||= "#{caller_class.quoted_table_name}.#{quoted_position_column}"
end
define_singleton_method :decrement_sequentially do
pluck(primary_key).each do |id|
where(primary_key => id).decrement_all
end
end
define_singleton_method :increment_sequentially do
pluck(primary_key).each do |id|
where(primary_key => id).increment_all
end
end
define_singleton_method :decrement_all do
update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} - 1)"
end
define_singleton_method :increment_all do
update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} + 1)"
end
define_singleton_method :update_all_with_touch do |updates|
updates += touch_record_sql if touch_on_update
update_all(updates)
end
private
define_singleton_method :touch_record_sql do
new.touch_record_sql
end
end
end
def self.define_instance_methods(caller_class, position_column)
caller_class.class_eval do
attr_reader :position_changed
define_method :position_column do
position_column
end
define_method :"#{position_column}=" do |position|
self[position_column] = position
@position_changed = true
end
define_method :touch_record_sql do
cached_quoted_now = quoted_current_time_from_proper_timezone
timestamp_attributes_for_update_in_model.map do |attr|
", #{connection.quote_column_name(attr)} = #{cached_quoted_now}"
end.join
end
private
delegate :connection, to: self
def quoted_current_time_from_proper_timezone
connection.quote(connection.quoted_date(
current_time_from_proper_timezone))
end
end
end
def self.mass_assignment_protection_was_used_by_user?(caller_class)
caller_class.class_eval do
respond_to?(:accessible_attributes) and accessible_attributes.present?
end
end
def self.protect_attributes_from_mass_assignment(caller_class, position_column)
caller_class.class_eval do
attr_accessible position_column.to_sym
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/sequential_updates_method_definer.rb 0000644 0000041 0000041 00000001740 14042233640 032455 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner #:nodoc:
def self.call(caller_class, column, sequential_updates_option)
caller_class.class_eval do
define_method :sequential_updates? do
if !defined?(@sequential_updates)
if sequential_updates_option.nil?
table_exists =
if active_record_version_is?('>= 5')
caller_class.connection.data_source_exists?(caller_class.table_name)
else
caller_class.connection.table_exists?(caller_class.table_name)
end
index_exists = caller_class.connection.index_exists?(caller_class.table_name, column, unique: true)
@sequential_updates = table_exists && index_exists
else
@sequential_updates = sequential_updates_option
end
else
@sequential_updates
end
end
private :sequential_updates?
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/add_new_at_method_definer.rb 0000644 0000041 0000041 00000000372 14042233640 030643 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord::Acts::List::AddNewAtMethodDefiner #:nodoc:
def self.call(caller_class, add_new_at)
caller_class.class_eval do
define_method :add_new_at do
add_new_at
end
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/no_update.rb 0000644 0000041 0000041 00000007341 14042233640 025503 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord
module Acts
module List
module NoUpdate
def self.included(base)
base.extend ClassMethods
end
class ArrayTypeError < ArgumentError
def initialize
super("The first argument must be an array")
end
end
class DisparityClassesError < ArgumentError
def initialize
super("The first argument should contain ActiveRecord or ApplicationRecord classes")
end
end
module ClassMethods
# Lets you selectively disable all act_as_list database updates
# for the duration of a block.
#
# ==== Examples
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, -> { order(position: :asc) }
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
#
# acts_as_list scope: :todo_list
# end
#
# TodoItem.acts_as_list_no_update do
# TodoList.first.update(position: 2)
# end
#
# You can also pass an array of classes as an argument to disable database updates on just those classes.
# It can be any ActiveRecord class that has acts_as_list enabled.
#
# ==== Examples
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, -> { order(position: :asc) }
# acts_as_list
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# has_many :todo_attachments, -> { order(position: :asc) }
#
# acts_as_list scope: :todo_list
# end
#
# class TodoAttachment < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list scope: :todo_item
# end
#
# TodoItem.acts_as_list_no_update([TodoAttachment]) do
# TodoItem.find(10).update(position: 2)
# TodoAttachment.find(10).update(position: 1)
# TodoAttachment.find(11).update(position: 2)
# TodoList.find(2).update(position: 3) # For this instance the callbacks will be called because we haven't passed the class as an argument
# end
def acts_as_list_no_update(extra_classes = [], &block)
return raise ArrayTypeError unless extra_classes.is_a?(Array)
extra_classes << self
return raise DisparityClassesError unless active_record_objects?(extra_classes)
NoUpdate.apply_to(extra_classes, &block)
end
private
def active_record_objects?(extra_classes)
extra_classes.all? { |klass| klass.ancestors.include? ActiveRecord::Base }
end
end
class << self
def apply_to(klasses)
klasses.map {|klass| add_klass(klass)}
yield
ensure
klasses.map {|klass| remove_klass(klass)}
end
def applied_to?(klass)
!(klass.ancestors & extracted_klasses.keys).empty?
end
private
def extracted_klasses
Thread.current[:act_as_list_no_update] ||= {}
end
def add_klass(klass)
extracted_klasses[klass] = 0 unless extracted_klasses.key?(klass)
extracted_klasses[klass] += 1
end
def remove_klass(klass)
extracted_klasses[klass] -= 1
extracted_klasses.delete(klass) if extracted_klasses[klass] <= 0
end
end
def act_as_list_no_update?
NoUpdate.applied_to?(self.class)
end
end
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/scope_method_definer.rb 0000644 0000041 0000041 00000004407 14042233640 027672 0 ustar www-data www-data # frozen_string_literal: true
require "active_support/inflector"
module ActiveRecord::Acts::List::ScopeMethodDefiner #:nodoc:
extend ActiveSupport::Inflector
def self.call(caller_class, scope)
scope = idify(caller_class, scope) if scope.is_a?(Symbol)
caller_class.class_eval do
define_method :scope_name do
scope
end
if scope.is_a?(Symbol)
define_method :scope_condition do
{ scope => send(:"#{scope}") }
end
define_method :scope_changed? do
changed.include?(scope_name.to_s)
end
define_method :destroyed_via_scope? do
scope == (destroyed_by_association && destroyed_by_association.foreign_key.to_sym)
end
elsif scope.is_a?(Array)
define_method :scope_condition do
# The elements of the Array can be symbols, strings, or hashes.
# If symbols or strings, they are treated as column names and the current value is looked up.
# If hashes, they are treated as fixed values.
scope.inject({}) do |hash, column_or_fixed_vals|
if column_or_fixed_vals.is_a?(Hash)
fixed_vals = column_or_fixed_vals
hash.merge!(fixed_vals)
else
column = column_or_fixed_vals
hash.merge!({ column.to_sym => read_attribute(column.to_sym) })
end
end
end
define_method :scope_changed? do
(scope_condition.keys & changed.map(&:to_sym)).any?
end
define_method :destroyed_via_scope? do
scope_condition.keys.include? (destroyed_by_association && destroyed_by_association.foreign_key.to_sym)
end
else
define_method :scope_condition do
eval "%{#{scope}}"
end
define_method :scope_changed? do
false
end
define_method :destroyed_via_scope? do
false
end
end
self.scope :in_list, lambda { where("#{quoted_position_column_with_table_name} IS NOT NULL") }
end
end
def self.idify(caller_class, name)
return name if name.to_s =~ /_id$/
if caller_class.reflections.key?(name.to_s)
caller_class.reflections[name.to_s].foreign_key.to_sym
else
foreign_key(name).to_sym
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/top_of_list_method_definer.rb 0000644 0000041 0000041 00000000537 14042233640 031102 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord::Acts::List::TopOfListMethodDefiner #:nodoc:
def self.call(caller_class, top_of_list)
caller_class.class_eval do
define_singleton_method :acts_as_list_top do
top_of_list.to_i
end
define_method :acts_as_list_top do
top_of_list.to_i
end
end
end
end
acts_as_list-1.0.4/lib/acts_as_list/active_record/acts/callback_definer.rb 0000644 0000041 0000041 00000001444 14042233640 026753 0 ustar www-data www-data # frozen_string_literal: true
module ActiveRecord::Acts::List::CallbackDefiner #:nodoc:
def self.call(caller_class, add_new_at)
caller_class.class_eval do
before_validation :check_top_position, unless: :act_as_list_no_update?
before_destroy :reload, unless: Proc.new { new_record? || destroyed_via_scope? || act_as_list_no_update? }
after_destroy :decrement_positions_on_lower_items, unless: Proc.new { destroyed_via_scope? || act_as_list_no_update? }
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, unless: :act_as_list_no_update?
end
end
end
end
acts_as_list-1.0.4/Gemfile 0000644 0000041 0000041 00000000547 14042233640 015513 0 ustar www-data www-data source "http://rubygems.org"
gemspec
gem "rake"
gem "appraisal"
group :development do
gem "github_changelog_generator", "1.9.0"
end
group :test do
gem "minitest", "~> 5.0"
gem "timecop"
gem "mocha"
end
group :sqlite do
gem "sqlite3", "~> 1.4"
end
group :postgresql do
gem "pg", "~> 1.2.0"
end
group :mysql do
gem "mysql2", "~> 0.5.0"
end
acts_as_list-1.0.4/.github/ 0000755 0000041 0000041 00000000000 14042233640 015552 5 ustar www-data www-data acts_as_list-1.0.4/.github/FUNDING.yml 0000644 0000041 0000041 00000000101 14042233640 017357 0 ustar www-data www-data # These are supported funding model platforms
github: [brendon]
acts_as_list-1.0.4/MIT-LICENSE 0000644 0000041 0000041 00000002054 14042233640 015647 0 ustar www-data www-data Copyright (c) 2007 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.