Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ gemspec
gem 'rspec'
gem 'rake'
gem 'simplecov'

gem 'rr'
78 changes: 65 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -27,27 +28,78 @@ 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 url_for(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 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
```

# 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
include BatchActions
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
```

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`.
`#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. Action name could be overriden with
`:action_name` param.

## 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 `resource_class` and scope from `end_of_association_chain`.

## 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.
2. autoinclude it to actioncontroller inside railtie.
3. :tirgger param must be a proc.

## Contributing

Expand Down
27 changes: 2 additions & 25 deletions lib/batch_actions.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,13 @@
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 instance_exec(&condition)
allowed << keyword
end
end

allowed
end

def batch_action
action = params[:name]

raise "action is not allowed" unless batch_actions.include? action.to_sym

send(:"batch_#{action}")
self.class.batch_actions.batch_actions
end

def self.included(base)
base.extend ClassMethods

if defined?(InheritedResources::Base) &&
base < InheritedResources::Base
base.batch_model base.resource_class
end
end
end
61 changes: 9 additions & 52 deletions lib/batch_actions/class_methods.rb
Original file line number Diff line number Diff line change
@@ -1,60 +1,17 @@
module BatchActions
module ClassMethods
def batch_model(klass)
@batch_model = klass
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
else
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
model.where(:id => params[:ids])
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

@batch_actions[keyword] = condition

define_method(:"batch_#{keyword}") do
result = instance_exec(&condition)

raise "action is not allowed" unless result
def batch_actions(&block)
@batch_actions ||= BatchActions::Context.new
@batch_actions.configure(self, &block) if block_given?
@batch_actions
end

objects = instance_exec(model, &scope)
apply.call(objects)
def batch_action(action, options = {})
batch_actions do
batch_action action, options
end
end

end
end
97 changes: 97 additions & 0 deletions lib/batch_actions/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
module BatchActions
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)
@controller_class = controller
instance_exec(&block)
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
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
trigger = options[:trigger] || name
model = options[:model] || @model

do_batch_stuff = block || ->(objects) do
results = objects.map do |object|
[object, object.send(batch_method)]
end
Hash[results]
end

@controller_class.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

@batch_actions[action_name] = trigger
end

def dispatch_action(name = 'batch_action')
@controller_class.class_eval do
define_method name do
batch_actions.detect do |action, trigger|
if params.key?(trigger)
send(action)
end
end
end
end
end

def default_scope
->(model, ids) do
tail = if self.class.respond_to?(:resource_class) && model.nil?
end_of_association_chain
else
model
end
tail or raise ArgumentError, 'You must specify batch_actions#model to apply batch action on'
tail.where(id: ids)
end
end

def default_response
->() do
respond_with(@objects, location: url_for(action: :index))
end
end
end
end
2 changes: 1 addition & 1 deletion lib/batch_actions/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module BatchActions
VERSION = "0.0.1"
VERSION = "0.0.2"
end
Loading