# 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) status = nil ActiveRecord::Base.transaction do object = assign_attributes(object, attributes) run_update_callbacks object do status = save_resource(object) raise ActiveRecord::Rollback unless status end end status 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