Skip to content

Commit

Permalink
acts_as_list_no_update allows one to supply additional classes (#260)
Browse files Browse the repository at this point in the history
  • Loading branch information
IlkhamGaysin authored and brendon committed Mar 3, 2017
1 parent 024ffe0 commit 1f6175d
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 60 deletions.
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,27 @@ 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:

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 = 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.
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.

Expand Down Expand Up @@ -132,7 +132,34 @@ TodoItem.acts_as_list_no_update do
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.
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
```

## Versions
Version `0.9.0` adds `acts_as_list_no_update` (https://github.com/swanandp/acts_as_list/pull/244) and compatibility with not-null and uniqueness constraints on the database (https://github.com/swanandp/acts_as_list/pull/246). These additions shouldn't break compatibility with existing implementations.
Expand All @@ -152,7 +179,7 @@ All versions `0.1.5` onwards require Rails 3.0.x and higher.
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
Expand Down
86 changes: 73 additions & 13 deletions lib/acts_as_list/active_record/acts/no_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,99 @@ module List
module NoUpdate
extend ActiveSupport::Concern

class ArrayTypeError < SyntaxError
def initialize
super("The first argument must be an array")
end
end

class DisparityClassesError < NotImplementedError
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
# ActiveRecord::Acts::List.acts_as_list_no_update do
# TodoList....
# end
#
# TodoList.acts_as_list_no_update do
# TodoList....
# end
# 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
#
def acts_as_list_no_update(&block)
NoUpdate.apply_to(self, &block)
# 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(klass)
klasses.push(klass)
def apply_to(klasses)
extracted_klasses.push(*klasses)
yield
ensure
klasses.pop
extracted_klasses.clear
end

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

private

def klasses
def extracted_klasses
Thread.current[:act_as_list_no_update] ||= []
end
end
Expand Down
27 changes: 26 additions & 1 deletion test/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,31 @@
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

# Returns true if ActiveRecord is rails 3, 4 version
def rails_3
defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
end

def rails_4
defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 4
end

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"

# ActiveRecord::Base.logger = Logger.new(STDOUT)
Expand All @@ -30,4 +55,4 @@ def assert_equal_or_nil(a, b)
else
assert_equal a, b
end
end
end
13 changes: 1 addition & 12 deletions test/test_joined_list.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
require 'helper'

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

class Section < ActiveRecord::Base
has_many :items
acts_as_list
Expand Down Expand Up @@ -37,14 +33,7 @@ def setup
end

def teardown
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
teardown_db
super
end
end
Expand Down
29 changes: 2 additions & 27 deletions test/test_list.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# NOTE: following now done in helper.rb (better Readability)
require 'helper'

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 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"

Expand All @@ -25,7 +21,7 @@ def setup_db(position_options = {})
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
Expand Down Expand Up @@ -57,27 +53,6 @@ def setup_db_with_default
setup_db default: 0
end

# Returns true if ActiveRecord is rails3,4 version
def rails_3
defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
end

def rails_4
defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 4
end

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

class Mixin < ActiveRecord::Base
self.table_name = 'mixins'
end
Expand Down

0 comments on commit 1f6175d

Please sign in to comment.