app/controllers/active_admin/resource_controller/data_access.rb
# frozen_string_literal: true
module ActiveAdmin
class ResourceController < BaseController
# This module overrides most of the data access methods in Inherited
# Resources to provide Active Admin with it's data.
#
# The module also deals with authorization and resource callbacks.
#
module DataAccess
def self.included(base)
base.class_exec do
include Callbacks
include ScopeChain
define_active_admin_callbacks :build, :create, :update, :save, :destroy
helper_method :current_scope
end
end
protected
COLLECTION_APPLIES = [
:authorization_scope,
:filtering,
:scoping,
:sorting,
:includes,
:pagination,
:collection_decorator
].freeze
# Retrieve, memoize and authorize the current collection from the db. This
# method delegates the finding of the collection to #find_collection.
#
# Once #collection has been called, the collection is available using
# either the @collection instance variable or an instance variable named
# after the resource that the collection is for. eg: Post => @post.
#
# @return [ActiveRecord::Relation] The collection for the index
def collection
get_collection_ivar || begin
collection = find_collection
authorize! Authorization::READ, active_admin_config.resource_class
set_collection_ivar collection
end
end
# Does the actual work of retrieving the current collection from the db.
# This is a great method to override if you would like to perform
# some additional db # work before your controller returns and
# authorizes the collection.
#
# @return [ActiveRecord::Relation] The collection for the index
def find_collection(options = {})
collection = scoped_collection
collection_applies(options).each do |applyer|
collection = send(:"apply_#{applyer}", collection)
end
collection
end
# Override this method in your controllers to modify the start point
# of our searches and index.
#
# This method should return an ActiveRecord::Relation object so that
# the searching and filtering can be applied on top
#
# Note, unless you are doing something special, you should use the
# scope_to method from the Scoping module instead of overriding this
# method.
def scoped_collection
end_of_association_chain
end
# Retrieve, memoize and authorize a resource based on params[:id]. The
# actual work of finding the resource is done in #find_resource.
#
# This method is used on all the member actions:
#
# * show
# * edit
# * update
# * destroy
#
# @return [ActiveRecord::Base] An active record object
def resource
get_resource_ivar || begin
resource = find_resource
resource = apply_decorations(resource)
authorize_resource! resource
set_resource_ivar resource
end
end
# Does the actual work of finding a resource in the database. This
# method uses the finder method as defined in InheritedResources.
#
# @return [ActiveRecord::Base] An active record object.
def find_resource
scoped_collection.send method_for_find, params[:id]
end
# Builds, memoize and authorize a new instance of the resource. The
# actual work of building the new instance is delegated to the
# #build_new_resource method.
#
# This method is used to instantiate and authorize new resources in the
# new and create controller actions.
#
# @return [ActiveRecord::Base] An un-saved active record base object
def build_resource
get_resource_ivar || begin
resource = build_new_resource
resource = apply_decorations(resource)
resource = assign_attributes(resource, resource_params)
run_build_callbacks resource
authorize_resource! resource
set_resource_ivar resource
end
end
# Builds a new resource. This method uses the method_for_build provided
# by Inherited Resources.
#
# @return [ActiveRecord::Base] An un-saved active record base object
def build_new_resource
apply_authorization_scope(scoped_collection).send(
method_for_build,
*resource_params.map { |params| params.slice(active_admin_config.resource_class.inheritance_column) }
)
end
# Calls all the appropriate callbacks and then creates the new resource.
#
# @param [ActiveRecord::Base] object The new resource to create
#
# @return [void]
def create_resource(object)
run_create_callbacks object do
save_resource(object)
end
end
# Calls all the appropriate callbacks and then saves the new resource.
#
# @param [ActiveRecord::Base] object The new resource to save
#
# @return [void]
def save_resource(object)
run_save_callbacks object do
object.save
end
end
# Update an object with the given attributes. Also calls the appropriate
# callbacks for update action.
#
# @param [ActiveRecord::Base] object The instance to update
#
# @param [Array] attributes An array with the attributes in the first position
# and the Active Record "role" in the second. The role
# may be set to nil.
#
# @return [void]
def update_resource(object, attributes)
object = assign_attributes(object, attributes)
run_update_callbacks object do
save_resource(object)
end
end
# Destroys an object from the database and calls appropriate callbacks.
#
# @return [void]
def destroy_resource(object)
run_destroy_callbacks object do
object.destroy
end
end
#
# Collection Helper Methods
#
# Gives the authorization library a change to pre-scope the collection.
#
# In the case of the CanCan adapter, it calls `#accessible_by` on
# the collection.
#
# @param [ActiveRecord::Relation] collection The collection to scope
#
# @return [ActiveRecord::Relation] a scoped collection of query
def apply_authorization_scope(collection)
action_name = action_to_permission(params[:action])
active_admin_authorization.scope_collection(collection, action_name)
end
def apply_sorting(chain)
params[:order] ||= active_admin_config.sort_order
order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order])
if order_clause.valid?
order_clause.apply(chain)
else
chain # just return the chain
end
end
# Applies any Ransack search methods to the currently scoped collection.
# Both `search` and `ransack` are provided, but we use `ransack` to prevent conflicts.
def apply_filtering(chain)
@search = chain.ransack(params[:q] || {}, auth_object: active_admin_authorization)
@search.result
end
def apply_scoping(chain)
@collection_before_scope = chain
if current_scope
scope_chain(current_scope, chain)
else
chain
end
end
def apply_includes(chain)
if active_admin_config.includes.any?
chain.includes *active_admin_config.includes
else
chain
end
end
def collection_before_scope
@collection_before_scope
end
def current_scope
@current_scope ||= if params[:scope]
active_admin_config.get_scope_by_id(params[:scope])
else
active_admin_config.default_scope(self)
end
end
def apply_pagination(chain)
# skip pagination if CSV format was requested
return chain if params["format"] == "csv"
# skip pagination if already was paginated by scope
return chain if chain.respond_to?(:total_pages)
page = params[Kaminari.config.param_name]
paginate(chain, page, per_page)
end
def collection_applies(options = {})
only = Array(options.fetch(:only, COLLECTION_APPLIES))
except = Array(options.fetch(:except, []))
COLLECTION_APPLIES & only - except
end
def in_paginated_batches(&block)
ActiveRecord::Base.uncached do
(1..paginated_collection.total_pages).each do |page|
paginated_collection(page).each do |resource|
yield apply_decorator(resource)
end
end
end
end
def per_page
if active_admin_config.paginate
dynamic_per_page || configured_per_page
else
active_admin_config.max_per_page
end
end
def dynamic_per_page
params[:per_page] || @per_page
end
def configured_per_page
Array(active_admin_config.per_page).first
end
# @param resource [ActiveRecord::Base]
# @param attributes [Array<Hash]
# @return [ActiveRecord::Base] resource
#
def assign_attributes(resource, attributes)
if resource.respond_to?(:assign_attributes)
resource.assign_attributes(*attributes)
else
resource.attributes = attributes[0]
end
resource
end
# @param resource [ActiveRecord::Base]
# @return [ActiveRecord::Base] resource
#
def apply_decorations(resource)
apply_decorator(resource)
end
# @return [String]
def smart_resource_url
if create_another?
new_resource_url(create_another: params[:create_another])
else
super
end
end
private
# @return [Boolean] true if user requested to create one more
# resource after creating this one.
def create_another?
params[:create_another].present?
end
def paginated_collection(page_no = 1)
paginate(collection, page_no, batch_size)
end
def paginate(chain, page, per_page)
page_method_name = Kaminari.config.page_method_name
chain.public_send(page_method_name, page).per(per_page)
end
def batch_size
1000
end
end
end
end