From 3d17f8bf5627dc983b4c91c4b573677559812fd3 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Thu, 20 Jun 2013 22:27:57 +0400 Subject: [PATCH 01/22] Get model class from inherited resources as it described in README --- lib/batch_actions/class_methods.rb | 2 ++ spec/batch_actions_spec.rb | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 748009d..9575eb8 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -11,6 +11,8 @@ def batch_action(keyword, opts = {}, &block) model = opts[:model] elsif !@batch_model.nil? model = @batch_model + elsif respond_to?(:resource_class) + model = resource_class else raise ArgumentError, "model must be specified" end diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index 19ed3e0..ca911a6 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -27,6 +27,17 @@ end.to raise_error(ArgumentError) end + it "sets a model from inherited_resources" do + expect do + mock_controller do + def self.resource_class + TestModel + end + batch_action :test1 + end + end.not_to raise_error(ArgumentError) + end + it "allows per-action override of a model" do ctrl = mock_controller( :ids => [ 1 ] From a716b064a14385be2299eaf88915bf26ac811345 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Thu, 20 Jun 2013 22:36:05 +0400 Subject: [PATCH 02/22] Fixed spec --- lib/batch_actions.rb | 5 ----- spec/batch_actions_spec.rb | 18 +++++------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/batch_actions.rb b/lib/batch_actions.rb index 6b48442..04cdd21 100644 --- a/lib/batch_actions.rb +++ b/lib/batch_actions.rb @@ -27,10 +27,5 @@ def batch_action def self.included(base) base.extend ClassMethods - - if defined?(InheritedResources::Base) && - base < InheritedResources::Base - base.batch_model base.resource_class - end end end diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index ca911a6..468c93b 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -27,17 +27,6 @@ end.to raise_error(ArgumentError) end - it "sets a model from inherited_resources" do - expect do - mock_controller do - def self.resource_class - TestModel - end - batch_action :test1 - end - end.not_to raise_error(ArgumentError) - end - it "allows per-action override of a model" do ctrl = mock_controller( :ids => [ 1 ] @@ -127,10 +116,13 @@ def self.resource_class it "supports InheritedResources" do expect do - mock_controller(:parent => InheritedResources::Base) do + mock_controller do + def self.resource_class + TestModel + end batch_action :test1 end - end.to_not raise_error + end.not_to raise_error(ArgumentError) end it "supports CanCan" do From 981c40f6dfdb12327c9f387eb962b9ad812d4563 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Thu, 20 Jun 2013 22:54:43 +0400 Subject: [PATCH 03/22] Other way to determine batch actinon --- lib/batch_actions.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/batch_actions.rb b/lib/batch_actions.rb index 04cdd21..0f75793 100644 --- a/lib/batch_actions.rb +++ b/lib/batch_actions.rb @@ -18,9 +18,9 @@ def batch_actions end def batch_action - action = params[:name] + action = params[:name] || (batch_actions & params.keys.map(&:to_sym)).first - raise "action is not allowed" unless batch_actions.include? action.to_sym + batch_actions.include?(action.try(:to_sym)) or raise "action is not allowed" send(:"batch_#{action}") end From 4d71e86d7254660ea0f7a1df567bc2b5231e252b Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Thu, 20 Jun 2013 23:03:20 +0400 Subject: [PATCH 04/22] End of assoc chain added to scope --- lib/batch_actions.rb | 3 ++- lib/batch_actions/class_methods.rb | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/batch_actions.rb b/lib/batch_actions.rb index 0f75793..cdf94d6 100644 --- a/lib/batch_actions.rb +++ b/lib/batch_actions.rb @@ -18,7 +18,8 @@ def batch_actions end def batch_action - action = params[:name] || (batch_actions & params.keys.map(&:to_sym)).first + batch_action_button = (batch_actions & params.keys.map(&:to_sym)).first + action = params[:name] || batch_action_button batch_actions.include?(action.try(:to_sym)) or raise "action is not allowed" diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 9575eb8..036cc4f 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -31,7 +31,12 @@ def batch_action(keyword, opts = {}, &block) scope = opts[:scope] else scope = ->(model) do - model.where(:id => params[:ids]) + tail = if respond_to?(:end_of_association_chain) + end_of_association_chain + else + model + end + tail.where(:id => params[:ids]) end end From 9d965e137ca7cb57c68ff4ca66b734510092efbc Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Thu, 20 Jun 2013 23:16:21 +0400 Subject: [PATCH 05/22] Use end_of_association_chain instead of a model --- lib/batch_actions.rb | 3 ++- lib/batch_actions/class_methods.rb | 6 ++---- spec/batch_actions_spec.rb | 8 +++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/batch_actions.rb b/lib/batch_actions.rb index cdf94d6..c09f7cd 100644 --- a/lib/batch_actions.rb +++ b/lib/batch_actions.rb @@ -21,7 +21,8 @@ def batch_action batch_action_button = (batch_actions & params.keys.map(&:to_sym)).first action = params[:name] || batch_action_button - batch_actions.include?(action.try(:to_sym)) or raise "action is not allowed" + (not(action.nil?) && batch_actions.include?(action.to_sym)) or + raise "batch action #{action} is not defined" send(:"batch_#{action}") end diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 036cc4f..f877396 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -11,10 +11,8 @@ def batch_action(keyword, opts = {}, &block) model = opts[:model] elsif !@batch_model.nil? model = @batch_model - elsif respond_to?(:resource_class) - model = resource_class - else - raise ArgumentError, "model must be specified" + elsif not(respond_to?(:resource_class)) + raise ArgumentError, 'model must be specified' end if block_given? diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index 468c93b..bf7a530 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -116,12 +116,17 @@ it "supports InheritedResources" do expect do - mock_controller do + ctrl = mock_controller do def self.resource_class TestModel end + def end_of_association_chain + resource_class + end + batch_action :test1 end + ctrl.batch_test1 end.not_to raise_error(ArgumentError) end @@ -155,4 +160,5 @@ def can?(keyword, model) ctrl.batch_action end end + end From d74fc3a57dc4f2dd2bd9ee4a77fa44f9d528a3cf Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Thu, 20 Jun 2013 23:20:20 +0400 Subject: [PATCH 06/22] Removed unuseful cancan if for batch action (sorry for that issue) --- lib/batch_actions/class_methods.rb | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index f877396..5c61f4c 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -38,17 +38,7 @@ def batch_action(keyword, opts = {}, &block) end end - if opts.include? :if - condition = opts[:if] - else - condition = ->() do - if self.respond_to? :can? - can? keyword, model - else - true - end - end - end + condition = opts[:if] @batch_actions[keyword] = condition From 9f5cf7795ca2ca3891b8a57c4814c77047adb1cf Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Fri, 21 Jun 2013 00:22:58 +0400 Subject: [PATCH 07/22] Removed default cancan condition from #batch_action (unuseful because batch methods exists in controllers) --- README.md | 11 ++++++++++- lib/batch_actions.rb | 2 +- lib/batch_actions/class_methods.rb | 9 +++++---- spec/batch_actions_spec.rb | 15 --------------- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 22bda4f..97b109d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,16 @@ class PostController < ApplicationController end ``` -Note that you can omit `batch_model` call if you use the [inherited_resources](https://github.com/josevalim/inherited_resources) gem. It grabs your model class from `resource_class`. +## CanCan + +Because of every batch_action creates action called `batch_#{name}`, you can +control access rights with CanCan. + +## InheritedResources + +Note that you can omit `batch_model` call if you use the [inherited_resources](https://github.com/josevalim/inherited_resources) gem. It grabs scope from `end_of_association_chain`. + +## Notes There's one more important thing to know: set of active batch actions can be retrieved from controller by calling `batch_actions` on controller instance. diff --git a/lib/batch_actions.rb b/lib/batch_actions.rb index c09f7cd..01b3d53 100644 --- a/lib/batch_actions.rb +++ b/lib/batch_actions.rb @@ -9,7 +9,7 @@ def batch_actions allowed = [] actions.each do |keyword, condition| - if instance_exec(&condition) + if condition.nil? || instance_exec(&condition) allowed << keyword end end diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 5c61f4c..0c18fbd 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -38,14 +38,15 @@ def batch_action(keyword, opts = {}, &block) end end - condition = opts[:if] + condition = opts[:if] if opts[:if] @batch_actions[keyword] = condition define_method(:"batch_#{keyword}") do - result = instance_exec(&condition) - - raise "action is not allowed" unless result + if condition + result = instance_exec(&condition) + raise "action is not allowed" unless result + end objects = instance_exec(model, &scope) apply.call(objects) diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index bf7a530..6fb0ab7 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -130,21 +130,6 @@ def end_of_association_chain end.not_to raise_error(ArgumentError) end - it "supports CanCan" do - ctrl = mock_controller do - batch_model TestModel - - batch_action :test1 - batch_action :test2 - - def can?(keyword, model) - keyword == :test1 - end - end - - ctrl.batch_actions.should == [ :test1 ] - end - it "implements batch_action" do [ "test1", "test2" ].each do |action| ctrl = mock_controller( From c55eb52a327f812bdf0c8ae070b87ed4f457d786 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sat, 22 Jun 2013 22:55:06 +0400 Subject: [PATCH 08/22] Complete rewrite WIP --- lib/batch_actions/class_methods.rb | 4 +- lib/batch_actions/context.rb | 80 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 lib/batch_actions/context.rb diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 0c18fbd..76f28dc 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -1,7 +1,7 @@ module BatchActions module ClassMethods - def batch_model(klass) - @batch_model = klass + def batch_actions_defaults(defaults = {}) + @batch_actions_defaults = defaults end def batch_action(keyword, opts = {}, &block) diff --git a/lib/batch_actions/context.rb b/lib/batch_actions/context.rb new file mode 100644 index 0000000..b8340c5 --- /dev/null +++ b/lib/batch_actions/context.rb @@ -0,0 +1,80 @@ +module BatchActions + module Context + def initialize + @model = nil + @scope = default_scope + @respond_to = default_response + @param_name = :ids + end + + def configure(controller, &block) + @controller = controller + instance_exec(&block) + end + + def model(model) + @model = model + end + + def param_name(name) + @param_name = name + end + + def scope(&block) + block_given? or raise ArgumentError, 'Need a block for batch_actions#scope' + @scope = block + end + + def respond_to(&block) + block_given? or raise ArgumentError, 'Need a block for batch_actions#respond_to' + @respond_to = block + end + + def batch_action(name, options = {}, &block) + scope = options[:scope] || @scope + response = options[:respond_to] || @respond_to + param_name = options[:param_name] || @param_name + action_name = options[:action_name] || :"batch_#{name}" + batch_method = options[:batch_method] || options[:action_name] || name + + do_batch_stuff = block || ->() do + objects.map do |object| + object.send(batch_method) + end + end + + @controller.class_eval do + define_method action_name do + @ids = params[param_name] + @objects = instance_exec(model, ids, &scope) + @results = &do_batch_stuff.call(@objects) + + instance_exec(&response) + end + end + end + + def dispatch_action(name) + + end + + private + def default_scope + ->(model, ids) do + tail = if respond_to?(:end_of_association_chain) + end_of_association_chain + else + model or raise 'You must specify batch_actions#model to apply batch action on' + model + end + tail.where(id: ids) + end + end + + def default_response + ->() do + respond_with(@objects) + end + end + end +end \ No newline at end of file From 9c4b7fc787539ac13f855166ea39d3abbb89feca Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 16:39:32 +0400 Subject: [PATCH 09/22] Base functionality specs done --- Gemfile | 2 +- lib/batch_actions.rb | 24 +--- lib/batch_actions/class_methods.rb | 55 +-------- lib/batch_actions/context.rb | 48 +++++--- lib/batch_actions/version.rb | 2 +- spec/batch_actions_spec.rb | 175 +++++++++++------------------ spec/spec_helper.rb | 24 ++-- 7 files changed, 115 insertions(+), 215 deletions(-) diff --git a/Gemfile b/Gemfile index 6250fc9..cd93635 100644 --- a/Gemfile +++ b/Gemfile @@ -6,4 +6,4 @@ gemspec gem 'rspec' gem 'rake' gem 'simplecov' - +gem 'rr' diff --git a/lib/batch_actions.rb b/lib/batch_actions.rb index 01b3d53..b4edbbc 100644 --- a/lib/batch_actions.rb +++ b/lib/batch_actions.rb @@ -1,30 +1,10 @@ require "batch_actions/version" require "batch_actions/class_methods" +require "batch_actions/context" module BatchActions def batch_actions - return [] unless self.class.instance_variable_defined? :@batch_actions - - actions = self.class.instance_variable_get :@batch_actions - allowed = [] - - actions.each do |keyword, condition| - if condition.nil? || instance_exec(&condition) - allowed << keyword - end - end - - allowed - end - - def batch_action - batch_action_button = (batch_actions & params.keys.map(&:to_sym)).first - action = params[:name] || batch_action_button - - (not(action.nil?) && batch_actions.include?(action.to_sym)) or - raise "batch action #{action} is not defined" - - send(:"batch_#{action}") + self.class.batch_actions.batch_actions end def self.included(base) diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 76f28dc..5f1b7b1 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -1,56 +1,9 @@ module BatchActions module ClassMethods - def batch_actions_defaults(defaults = {}) - @batch_actions_defaults = defaults - end - - def batch_action(keyword, opts = {}, &block) - @batch_actions = {} if @batch_actions.nil? - - if opts.include? :model - model = opts[:model] - elsif !@batch_model.nil? - model = @batch_model - elsif not(respond_to?(:resource_class)) - raise ArgumentError, 'model must be specified' - end - - if block_given? - apply = block - else - apply = ->(objects) do - objects.each do |object| - object.send(keyword) - end - end - end - - if opts.include? :scope - scope = opts[:scope] - else - scope = ->(model) do - tail = if respond_to?(:end_of_association_chain) - end_of_association_chain - else - model - end - tail.where(:id => params[:ids]) - end - end - - condition = opts[:if] if opts[:if] - - @batch_actions[keyword] = condition - - define_method(:"batch_#{keyword}") do - if condition - result = instance_exec(&condition) - raise "action is not allowed" unless result - end - - objects = instance_exec(model, &scope) - apply.call(objects) - end + def batch_actions(&block) + @batch_actions ||= BatchActions::Context.new + @batch_actions.configure(self, &block) if block_given? + @batch_actions end end end diff --git a/lib/batch_actions/context.rb b/lib/batch_actions/context.rb index b8340c5..60a73e2 100644 --- a/lib/batch_actions/context.rb +++ b/lib/batch_actions/context.rb @@ -1,10 +1,14 @@ module BatchActions - module Context + class Context + attr_reader :batch_actions + def initialize @model = nil @scope = default_scope @respond_to = default_response @param_name = :ids + + @batch_actions = {} end def configure(controller, &block) @@ -12,14 +16,15 @@ def configure(controller, &block) instance_exec(&block) end - def model(model) - @model = model - end - + private def param_name(name) @param_name = name end + def model(resource_class) + @model = resource_class + end + def scope(&block) block_given? or raise ArgumentError, 'Need a block for batch_actions#scope' @scope = block @@ -36,36 +41,47 @@ def batch_action(name, options = {}, &block) param_name = options[:param_name] || @param_name action_name = options[:action_name] || :"batch_#{name}" batch_method = options[:batch_method] || options[:action_name] || name + trigger = options[:trigger] || name + model = options[:model] || @model - do_batch_stuff = block || ->() do - objects.map do |object| - object.send(batch_method) + do_batch_stuff = block || ->(objects) do + results = objects.map do |object| + [object, object.send(batch_method)] end + Hash[results] end @controller.class_eval do define_method action_name do @ids = params[param_name] - @objects = instance_exec(model, ids, &scope) - @results = &do_batch_stuff.call(@objects) + @objects = instance_exec(model, @ids, &scope) + @results = do_batch_stuff.call(@objects) instance_exec(&response) end end - end - def dispatch_action(name) + @batch_actions[action_name] = trigger + end + def dispatch_action(name = 'batch_action') + @controller.class_eval do + define_method name do + batch_actions.detect do |action, trigger| + if params.key?(trigger) + send(action) + end + end + end + end end - private def default_scope ->(model, ids) do - tail = if respond_to?(:end_of_association_chain) + tail = if respond_to?(:end_of_association_chain) && model.blank? end_of_association_chain else - model or raise 'You must specify batch_actions#model to apply batch action on' - model + model or raise ArgumentError, 'You must specify batch_actions#model to apply batch action on' end tail.where(id: ids) end diff --git a/lib/batch_actions/version.rb b/lib/batch_actions/version.rb index 02667f0..c9d621f 100644 --- a/lib/batch_actions/version.rb +++ b/lib/batch_actions/version.rb @@ -1,3 +1,3 @@ module BatchActions - VERSION = "0.0.1" + VERSION = "0.0.2" end diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index 6fb0ab7..f38d5e1 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -1,149 +1,100 @@ require 'spec_helper' describe BatchActions do - it "generates batch actions" do - ctrl = mock_controller( - :ids => [ 1, 2 ] - ) do - batch_model TestModel - batch_action :test1 - end - - times = 0 + it 'generates batch actions' do + params = {:ids => [ 1, 2 ]} - TestModel.should_receive(:where).with({ :id => [ 1, 2 ]}).and_call_original - TestModel.any_instance.stub(:test1) { times += 1 } - - ctrl.batch_test1 - - times.should == 2 - end - - it "requires a model to be specified" do - expect do - mock_controller do + ctrl = mock_controller(params) do + batch_actions do + model TestModel batch_action :test1 end - end.to raise_error(ArgumentError) - end - - it "allows per-action override of a model" do - ctrl = mock_controller( - :ids => [ 1 ] - ) do - batch_model TestModel - - batch_action :test1 - batch_action :test2, :model => TestModel2 end - TestModel.should_receive(:where).with({ :id => [ 1 ]}).and_call_original - TestModel2.should_receive(:where).with({ :id => [ 1 ]}).and_call_original - - TestModel.any_instance.should_receive(:test1).and_return(nil) - TestModel2.any_instance.should_receive(:test2).and_return(nil) + mock.proxy(TestModel).where(id: params[:ids]).once + any_instance_of(TestModel) do |klass| + mock(klass).test1.times(params[:ids].length) + end - ctrl.batch_test1 - ctrl.batch_test2 + ctrl.batch_test1.should == 'Default response' end - it "allows to specify scope" do - scope_called = false - - instance = TestModel.new - instance.should_receive(:test1).and_return(nil) - - ctrl = mock_controller( - :ids => [ 1 ] - ) do - batch_model TestModel - - batch_action :test1, :scope => ->(model) do - scope_called = true - - [ instance ] + it 'requires a model to be specified' do + ctrl = mock_controller do + batch_actions do + batch_action :test1 end end - ctrl.batch_test1 - - scope_called.should be_true + -> { ctrl.batch_test1 }.should raise_error(ArgumentError) end - it "allows to override default apply" do - block_called = nil - - ctrl = mock_controller( - :ids => [ 1 ] - ) do - batch_model TestModel + it 'does not require to specify model for inherited_resources' do + ctrl = mock_controller do + def self.resource_class + TestModel + end + def end_of_association_chain + resource_class + end - batch_action(:test1) do |objects| - block_called = objects + batch_actions do + batch_action :test1 end end - - ctrl.batch_test1 - - block_called.should_not be_nil - block_called.length.should == 1 + -> { ctrl.batch_test1 }.should_not raise_error(ArgumentError) end - it "supports :if" do + it 'allows per-action override of params' do ctrl = mock_controller( - :ids => [ 1 ] + :ids_eq => [1, 2], + :the_ids => [1, 2] ) do - batch_model TestModel - - batch_action :test, :if => ->() { false } - end - - expect { ctrl.batch_test1 }.to raise_error - ctrl.batch_actions.should be_empty - end + batch_actions do + model TestModel - it "implements batch_actions" do - ctrl = mock_controller do - batch_model TestModel + param_name :the_ids + scope { |model, ids| model.where(other_id: ids) } + respond_to { 'Correct response' } - batch_action :test1 - batch_action :test2 - batch_action :test3, :if => ->() { false } + batch_action :test1 + batch_action :test2, + param_name: :ids_eq, + model: TestModel2, + action_name: 'test_action', + batch_method: 'test_method', + scope: ->(model, ids) { model.somewhere(id: ids) }, + respond_to: -> { 'Test response overriden' } + end end - ctrl.batch_actions.should == [ :test1, :test2 ] - end + mock.proxy(TestModel).where(other_id: [1, 2]).once + any_instance_of(TestModel) do |klass| + mock(klass).test1.twice + end - it "supports InheritedResources" do - expect do - ctrl = mock_controller do - def self.resource_class - TestModel - end - def end_of_association_chain - resource_class - end + mock.proxy(TestModel2).somewhere(id: [1, 2]).once + any_instance_of(TestModel2) do |klass| + mock(klass).test_method.twice + end - batch_action :test1 - end - ctrl.batch_test1 - end.not_to raise_error(ArgumentError) + ctrl.batch_test1.should == 'Correct response' + ctrl.test_action.should == 'Test response overriden' end - it "implements batch_action" do - [ "test1", "test2" ].each do |action| - ctrl = mock_controller( - :ids => [ 1 ], - :name => action - ) do - batch_model TestModel + it 'implements #batch_actions' do + ctrl = mock_controller do + batch_actions do batch_action :test1 batch_action :test2 + batch_action :test3 end - - TestModel.any_instance.should_receive(action.to_sym).and_return(nil) - ctrl.batch_action end - end + ctrl.batch_actions.should == { + batch_test1: :test1, + batch_test2: :test2, + batch_test3: :test3 + } + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 78396a9..06bdf10 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,25 +1,17 @@ require 'simplecov' -SimpleCov.start +SimpleCov.start require 'batch_actions' class TestModel def self.where(query) - query[:id].map { self.new } + query[query.keys.first].map { self.new } end end class TestModel2 - def self.where(query) - query[:id].map { self.new } - end -end - -module InheritedResources - class Base - def self.resource_class - TestModel - end + def self.somewhere(query) + query[query.keys.first].map { self.new } end end @@ -32,6 +24,10 @@ def mock_controller(params = {}, &block) def params self.class.instance_variable_get :@params end + + def respond_with(object) + "Default response" + end end mock_class.class_exec(&block) @@ -39,3 +35,7 @@ def params mock_class.new end + +RSpec.configure do |config| + config.mock_with :rr +end \ No newline at end of file From b779745016d319b286e2b21358d77960ad5d5472 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 16:48:47 +0400 Subject: [PATCH 10/22] Batch_dispatch mocked --- lib/batch_actions/context.rb | 2 +- spec/batch_actions_spec.rb | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/batch_actions/context.rb b/lib/batch_actions/context.rb index 60a73e2..de29c9f 100644 --- a/lib/batch_actions/context.rb +++ b/lib/batch_actions/context.rb @@ -78,7 +78,7 @@ def dispatch_action(name = 'batch_action') def default_scope ->(model, ids) do - tail = if respond_to?(:end_of_association_chain) && model.blank? + tail = if respond_to?(:end_of_association_chain) && model.nil? end_of_association_chain else model or raise ArgumentError, 'You must specify batch_actions#model to apply batch action on' diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index f38d5e1..929f61e 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -34,6 +34,7 @@ def self.resource_class TestModel end + def end_of_association_chain resource_class end @@ -97,4 +98,21 @@ def end_of_association_chain batch_test3: :test3 } end + + it 'implements dispatch action' do + ctrl = mock_controller(:test1 => true, ids: [1, 2]) do + batch_actions do + model TestModel + dispatch_action + batch_action :test1 + end + end + + any_instance_of(TestModel) do |klass| + mock(klass).test1.any_times + end + proxy(ctrl).batch_test1.once + + ctrl.batch_action + end end From 02dc92c77596dafafb619fabded87893235f406b Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 17:33:24 +0400 Subject: [PATCH 11/22] Definition of single batch action --- lib/batch_actions/class_methods.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index 5f1b7b1..d09de4d 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -5,5 +5,11 @@ def batch_actions(&block) @batch_actions.configure(self, &block) if block_given? @batch_actions end + + def batch_action(action, options = {}) + batch_actions do + batch_action action, options + end + end end end From e7f764c63d615ba75271c1a29c079994fa719765 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 17:34:42 +0400 Subject: [PATCH 12/22] Specing batch_action --- spec/batch_actions_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index 929f61e..8821723 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -39,11 +39,11 @@ def end_of_association_chain resource_class end - batch_actions do - batch_action :test1 - end + batch_action :test1 + batch_action :test2 end -> { ctrl.batch_test1 }.should_not raise_error(ArgumentError) + -> { ctrl.batch_test2 }.should_not raise_error(ArgumentError) end it 'allows per-action override of params' do From 2e82e6e3829aa82edfbcbfbe685382583c46a2a6 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 17:58:58 +0400 Subject: [PATCH 13/22] #resource_class instead of chain --- lib/batch_actions/class_methods.rb | 6 ++++-- lib/batch_actions/context.rb | 5 +++-- spec/batch_actions_spec.rb | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index d09de4d..aa1c88c 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -1,15 +1,17 @@ module BatchActions module ClassMethods + def batch_actions(&block) @batch_actions ||= BatchActions::Context.new @batch_actions.configure(self, &block) if block_given? @batch_actions end - def batch_action(action, options = {}) + def batch_action(action) batch_actions do - batch_action action, options + batch_action action end end + end end diff --git a/lib/batch_actions/context.rb b/lib/batch_actions/context.rb index de29c9f..4d56de7 100644 --- a/lib/batch_actions/context.rb +++ b/lib/batch_actions/context.rb @@ -78,11 +78,12 @@ def dispatch_action(name = 'batch_action') def default_scope ->(model, ids) do - tail = if respond_to?(:end_of_association_chain) && model.nil? + tail = if respond_to?(:resource_class) && model.nil? end_of_association_chain else - model or raise ArgumentError, 'You must specify batch_actions#model to apply batch action on' + model end + tail or raise ArgumentError, 'You must specify batch_actions#model to apply batch action on' tail.where(id: ids) end end diff --git a/spec/batch_actions_spec.rb b/spec/batch_actions_spec.rb index 8821723..0c747a5 100644 --- a/spec/batch_actions_spec.rb +++ b/spec/batch_actions_spec.rb @@ -35,8 +35,8 @@ def self.resource_class TestModel end - def end_of_association_chain - resource_class + def resource_class + self.class.resource_class end batch_action :test1 From 52beed7b5cce81309f2c7ca8efea9da5042765f6 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 18:18:22 +0400 Subject: [PATCH 14/22] fixed #respond_with --- lib/batch_actions/context.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/batch_actions/context.rb b/lib/batch_actions/context.rb index 4d56de7..9c8b7b7 100644 --- a/lib/batch_actions/context.rb +++ b/lib/batch_actions/context.rb @@ -90,7 +90,7 @@ def default_scope def default_response ->() do - respond_with(@objects) + respond_with(@objects, location: url_for(action: :index)) end end end From 1fa0e88d0985aa7b9ad1626e5fe3d0ceebd8233a Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 18:19:25 +0400 Subject: [PATCH 15/22] Fixed specs --- spec/spec_helper.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 06bdf10..1790dd5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,9 +25,13 @@ def params self.class.instance_variable_get :@params end - def respond_with(object) + def respond_with(object, *args) "Default response" end + + def url_for(*args) + "" + end end mock_class.class_exec(&block) From 5fe857c99f1373a116a82e64e97e4f9fa57c48b3 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 19:43:19 +0400 Subject: [PATCH 16/22] Readme fixed --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 97b109d..4273172 100644 --- a/README.md +++ b/README.md @@ -27,36 +27,75 @@ Or install it yourself as: class PostController < ApplicationController include BatchActions - batch_model Post + batch_actions do + model Post + + # Produces `#batch_publish` action. Requires `params[:ids]` to get affected + # instances and call `#publish` on them. + batch_action :publish + + # params[:batch_destroy] should be an array containing affected ids + batch_action :destroy, param_name: :batch_destroy + + # Produced controller action will be called `#mass_unpublish` (instead of + # `batch_unpublish by default). Method `#draft!` will be called for each + # affected instance. + batch_action :unpublish, action_name: :mass_unpublish, batch_method: :draft! + + # Affected objects will be got inside `#destroyed` scope, redirection will + # be done to params[:return_to] instead of action: :index + batch_action :restore, + scope: ->(model, ids) { Post.destroyed.where(id: ids) }, + respose: -> { + respond_to do |format| + format.html { redirect_to params[:return_to] } + end + } + + # Produces action `#do_batch` with dispatches batch action to concrete + # actions if you pass param named as batch action name ('destroy=true' + # for `batch_action :destroy`). You can change param name which triggers + # batch action with `:trigger` param of batch action. + dispatch_action(:do_batch) + end +end +``` - # Runs `model.publish` for every model from params[:ids] - batch_action :publish +## Inheritance - # Runs `model.destroy` for every model from params[:ids] or throws exception unless you can - batch_action :destroy, if: ->() { can? :destroy, Post } +Batch action options and batch actions could be inherited. - # Runs block for every model from params[:ids] - batch_action :specific do |objects| - objects.each{|x| x.specific!} +``` +class Admin::BaseController < ApplicationController + batch_actions do + param_name :ids_eq end +end - # Runs `model.resurrect` for every model from returned relation - batch_action :resurrect, :scope => ->(ids) { Post.where(other_ids: ids) } +class Admin::NewsController < Admin::BaseController + # You can omit `#batch_actions` call if you do not want to set options. + batch_action :destroy + batch_action :publish end ``` +`#batch_destroy` and `#batch_publish` will require `params[:ids_eq]` to work. + ## CanCan Because of every batch_action creates action called `batch_#{name}`, you can -control access rights with CanCan. +control access rights with CanCan. Action name could be overriden with +`:action_name` param. ## InheritedResources -Note that you can omit `batch_model` call if you use the [inherited_resources](https://github.com/josevalim/inherited_resources) gem. It grabs scope from `end_of_association_chain`. +Note that you can omit `model` call if you use the [inherited_resources](https://github.com/josevalim/inherited_resources) gem. It grabs scope from `end_of_association_chain`. -## Notes +## TODO -There's one more important thing to know: set of active batch actions can be retrieved from controller by calling `batch_actions` on controller instance. +1. call before and after filters for producted actions if they are called from + dispatcher. +1. implement flash messages with inherited_resources responders for example. ## Contributing From f8c44ee2816a58b52eea5ff76fc431c8855b2d93 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 19:44:30 +0400 Subject: [PATCH 17/22] README fix --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4273172..ce6d986 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Batch action options and batch actions could be inherited. ``` class Admin::BaseController < ApplicationController + include BatchActions batch_actions do param_name :ids_eq end @@ -96,6 +97,7 @@ Note that you can omit `model` call if you use the [inherited_resources](https:/ 1. call before and after filters for producted actions if they are called from dispatcher. 1. implement flash messages with inherited_resources responders for example. +2. autoinclude it to actioncontroller inside railtie. ## Contributing From cf414d16ae03db1f5bc2ac80b8d59e693c150693 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 19:46:03 +0400 Subject: [PATCH 18/22] Fixed readme --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ce6d986..8bf9421 100644 --- a/README.md +++ b/README.md @@ -30,19 +30,19 @@ class PostController < ApplicationController batch_actions do model Post - # Produces `#batch_publish` action. Requires `params[:ids]` to get affected - # instances and call `#publish` on them. + # Produces #batch_publish action. Requires params[:ids] to get affected + # instances and call #publish on them. batch_action :publish # params[:batch_destroy] should be an array containing affected ids batch_action :destroy, param_name: :batch_destroy - # Produced controller action will be called `#mass_unpublish` (instead of - # `batch_unpublish by default). Method `#draft!` will be called for each + # Produced controller action will be called #mass_unpublish (instead of + # batch_unpublish by default). Method `#draft!` will be called for each # affected instance. batch_action :unpublish, action_name: :mass_unpublish, batch_method: :draft! - # Affected objects will be got inside `#destroyed` scope, redirection will + # Affected objects will be got inside #destroyed scope, redirection will # be done to params[:return_to] instead of action: :index batch_action :restore, scope: ->(model, ids) { Post.destroyed.where(id: ids) }, @@ -52,10 +52,10 @@ class PostController < ApplicationController end } - # Produces action `#do_batch` with dispatches batch action to concrete - # actions if you pass param named as batch action name ('destroy=true' - # for `batch_action :destroy`). You can change param name which triggers - # batch action with `:trigger` param of batch action. + # Produces action #do_batch with dispatches batch action to concrete + # actions if you pass param named as batch action name ("destroy=true" + # for batch_action :destroy). You can change param name which triggers + # batch action with :trigger param of batch action. dispatch_action(:do_batch) end end @@ -74,7 +74,7 @@ class Admin::BaseController < ApplicationController end class Admin::NewsController < Admin::BaseController - # You can omit `#batch_actions` call if you do not want to set options. + # You can omit #batch_actions call if you do not want to set options. batch_action :destroy batch_action :publish end @@ -98,6 +98,7 @@ Note that you can omit `model` call if you use the [inherited_resources](https:/ dispatcher. 1. implement flash messages with inherited_resources responders for example. 2. autoinclude it to actioncontroller inside railtie. +3. :tirgger param must be a proc. ## Contributing From b98ad77d4eab309414036e2b4cb6ba61d131edf6 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 19:51:26 +0400 Subject: [PATCH 19/22] README fix --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8bf9421..1f6a5f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # BatchActions -This gem adds generic support for batch actions to Rails controllers. +This gem adds generic support for batch actions to Rails controllers and +InheritedResources. [![Travis CI](https://secure.travis-ci.org/grindars/batch_actions.png)](https://travis-ci.org/grindars/batch_actions) [![Code Climate](https://codeclimate.com/github/grindars/batch_actions.png)](https://codeclimate.com/github/grindars/batch_actions) @@ -38,12 +39,12 @@ class PostController < ApplicationController batch_action :destroy, param_name: :batch_destroy # Produced controller action will be called #mass_unpublish (instead of - # batch_unpublish by default). Method `#draft!` will be called for each + # batch_unpublish by default). Method #draft! will be called for each # affected instance. batch_action :unpublish, action_name: :mass_unpublish, batch_method: :draft! # Affected objects will be got inside #destroyed scope, redirection will - # be done to params[:return_to] instead of action: :index + # be done to params[:return_to] instead of url_for(action: :index) batch_action :restore, scope: ->(model, ids) { Post.destroyed.where(id: ids) }, respose: -> { @@ -52,10 +53,10 @@ class PostController < ApplicationController end } - # Produces action #do_batch with dispatches batch action to concrete - # actions if you pass param named as batch action name ("destroy=true" - # for batch_action :destroy). You can change param name which triggers - # batch action with :trigger param of batch action. + # Produces action #do_batch with dispatches request to concrete + # actions. Concrete action is determined by param named as action name + # ("destroy=true" for batch_action :destroy) or by :trigger option value + # ("call_destroy=true for batch_action :destroy, trigger: :call_destroy). dispatch_action(:do_batch) end end From 7e16f455de264e73493c00245f6f93c6980ce675 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Sun, 23 Jun 2013 19:53:10 +0400 Subject: [PATCH 20/22] README fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f6a5f6..236bee9 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ class PostController < ApplicationController # Produces action #do_batch with dispatches request to concrete # actions. Concrete action is determined by param named as action name # ("destroy=true" for batch_action :destroy) or by :trigger option value - # ("call_destroy=true for batch_action :destroy, trigger: :call_destroy). + # ("call_destroy=true" for batch_action :destroy, trigger: :call_destroy). dispatch_action(:do_batch) end end @@ -91,7 +91,7 @@ control access rights with CanCan. Action name could be overriden with ## InheritedResources -Note that you can omit `model` call if you use the [inherited_resources](https://github.com/josevalim/inherited_resources) gem. It grabs scope from `end_of_association_chain`. +Note that you can omit `model` call if you use the [inherited_resources](https://github.com/josevalim/inherited_resources) gem. It grabs scope from `resource_class` and scope from `end_of_association_chain`. ## TODO From 0e97f594f0537ed382672563481814dad2b37545 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Mon, 24 Jun 2013 06:15:02 +0400 Subject: [PATCH 21/22] Allow options for single batch action --- lib/batch_actions/class_methods.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/batch_actions/class_methods.rb b/lib/batch_actions/class_methods.rb index aa1c88c..e974682 100644 --- a/lib/batch_actions/class_methods.rb +++ b/lib/batch_actions/class_methods.rb @@ -7,9 +7,9 @@ def batch_actions(&block) @batch_actions end - def batch_action(action) + def batch_action(action, options = {}) batch_actions do - batch_action action + batch_action action, options end end From 74b50754361da942e226db0724a032fc01b7a5e0 Mon Sep 17 00:00:00 2001 From: Victor Sokolov Date: Fri, 12 Jul 2013 00:03:44 +0400 Subject: [PATCH 22/22] Better inherited resources recognition --- lib/batch_actions/context.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/batch_actions/context.rb b/lib/batch_actions/context.rb index 9c8b7b7..d6cd64a 100644 --- a/lib/batch_actions/context.rb +++ b/lib/batch_actions/context.rb @@ -12,7 +12,7 @@ def initialize end def configure(controller, &block) - @controller = controller + @controller_class = controller instance_exec(&block) end @@ -51,7 +51,7 @@ def batch_action(name, options = {}, &block) Hash[results] end - @controller.class_eval do + @controller_class.class_eval do define_method action_name do @ids = params[param_name] @objects = instance_exec(model, @ids, &scope) @@ -65,7 +65,7 @@ def batch_action(name, options = {}, &block) end def dispatch_action(name = 'batch_action') - @controller.class_eval do + @controller_class.class_eval do define_method name do batch_actions.detect do |action, trigger| if params.key?(trigger) @@ -78,7 +78,7 @@ def dispatch_action(name = 'batch_action') def default_scope ->(model, ids) do - tail = if respond_to?(:resource_class) && model.nil? + tail = if self.class.respond_to?(:resource_class) && model.nil? end_of_association_chain else model