diff --git a/app/controllers/administrate/application_controller.rb b/app/controllers/administrate/application_controller.rb
index 905385fad6..c38f652f41 100644
--- a/app/controllers/administrate/application_controller.rb
+++ b/app/controllers/administrate/application_controller.rb
@@ -1,300 +1,81 @@
module Administrate
class ApplicationController < ActionController::Base
+ include Administrate::Authorizable
+ include Administrate::CollectionPaginator
+ include Administrate::CollectionSearchable
+ include Administrate::CollectionSortable
+ include Administrate::ControllerDeprecator
+ include Administrate::ControllerI18n
+ include Administrate::DashboardManager
+ include Administrate::HtmlRenderer
+ include Administrate::ResourceManager
+
protect_from_forgery with: :exception
def index
- authorize_resource(resource_class)
- search_term = params[:search].to_s.strip
- resources = filter_resources(scoped_resource, search_term: search_term)
- resources = apply_collection_includes(resources)
- resources = order.apply(resources)
- resources = paginate_resources(resources)
- @resources = resources
- page = Administrate::Page::Collection.new(dashboard, order: order)
- page.context = self
- filters = Administrate::Search.new(scoped_resource, dashboard, search_term).valid_filters
-
- render locals: {
- resources: resources,
- search_term: search_term,
- page: page,
- show_search_bar: show_search_bar?,
- filters: filters
- }
+ respond_to do |format|
+ format.html { respond_to_index_html }
+ format.json { render json: collection_resources }
+ end
end
def show
- @resource = resource = requested_resource
- page = Administrate::Page::Show.new(dashboard, resource)
- page.context = self
- render locals: {
- page: page
- }
+ respond_to do |format|
+ format.html { respond_to_show_html }
+ format.json { render json: requested_resource }
+ end
end
def new
- @resource = resource = new_resource.tap do |resource|
- authorize_resource(resource)
+ respond_to do |format|
+ format.html { respond_to_new_html }
end
-
- page = Administrate::Page::Form.new(dashboard, resource)
- page.context = self
- render locals: {
- page: page
- }
end
def edit
- @resource = resource = requested_resource
- page = Administrate::Page::Form.new(dashboard, resource)
- page.context = self
- render locals: {
- page: page
- }
+ respond_to do |format|
+ format.html { respond_to_edit_html }
+ end
end
def create
- @resource = resource = new_resource(resource_params).tap do |resource|
- authorize_resource(resource)
- end
+ if save_built_resource
+ yield(built_resource) if block_given?
- if resource.save
- yield(resource) if block_given?
- redirect_to(
- after_resource_created_path(resource),
- notice: translate_with_resource("create.success")
- )
+ respond_to do |format|
+ format.html { respond_to_create_html }
+ end
else
- page = Administrate::Page::Form.new(dashboard, resource)
- page.context = self
- render :new, locals: {
- page: page
- }, status: :unprocessable_entity
+ respond_to do |format|
+ format.html { respond_to_create_error_html }
+ end
end
end
def update
- @resource = resource = requested_resource
- if resource.update(resource_params)
- redirect_to(
- after_resource_updated_path(resource),
- notice: translate_with_resource("update.success"),
- status: :see_other
- )
- else
- page = Administrate::Page::Form.new(dashboard, resource)
- page.context = self
- render :edit, locals: {
- page: page
- }, status: :unprocessable_entity
- end
- end
-
- def destroy
- @resource = resource = requested_resource
- if resource.destroy
- flash[:notice] = translate_with_resource("destroy.success")
- else
- flash[:error] = resource.errors.full_messages.join("
")
- end
- redirect_to after_resource_destroyed_path(resource), status: :see_other
- end
-
- private
-
- def filter_resources(resources, search_term:)
- Administrate::Search.new(
- resources,
- dashboard,
- search_term
- ).run
- end
-
- def after_resource_destroyed_path(_requested_resource)
- {action: :index}
- end
-
- def after_resource_created_path(requested_resource)
- [namespace, requested_resource]
- end
-
- def after_resource_updated_path(requested_resource)
- [namespace, requested_resource]
- end
-
- helper_method :nav_link_state
- def nav_link_state(resource)
- underscore_resource = resource.to_s.split("/").join("__")
- (resource_name.to_s.pluralize == underscore_resource) ? :active : :inactive
- end
-
- # Whether the named action route exists for the resource class.
- #
- # @param resource [Class, String, Symbol] A class of resources, or the name
- # of a class of resources.
- # @param action_name [String, Symbol] The name of an action that might be
- # possible to perform on a resource or resource class.
- # @return [Boolean] `true` if a route exists for the resource class and the
- # action. `false` otherwise.
- def existing_action?(resource, action_name)
- routes.include?([resource.to_s.underscore.pluralize, action_name.to_s])
- end
- helper_method :existing_action?
-
- def routes
- @routes ||= Namespace.new(namespace).routes.to_set
- end
-
- def records_per_page
- params[:per_page] || 20
- end
-
- def order
- @order ||= Administrate::Order.new(
- sorting_attribute,
- sorting_direction,
- sorting_column: sorting_column(
- dashboard_attribute(sorting_attribute)
- )
- )
- end
-
- def sorting_column(dashboard_attribute)
- return unless dashboard_attribute.try(:options)
-
- dashboard_attribute.options.fetch(:sorting_column) {
- dashboard_attribute.options.fetch(:order, nil)
- }
- end
-
- def dashboard_attribute(attribute)
- dashboard.attribute_types[attribute.to_sym] if attribute
- end
+ requested_resource.assign_attributes(resource_params)
- def sorting_attribute
- sorting_params.fetch(:order) { default_sorting_attribute }
- end
-
- def default_sorting_attribute
- nil
- end
-
- def sorting_direction
- sorting_params.fetch(:direction) { default_sorting_direction }
- end
-
- def default_sorting_direction
- nil
- end
-
- def sorting_params
- Hash.try_convert(request.query_parameters[resource_name]) || {}
- end
-
- def dashboard
- @dashboard ||= dashboard_class.new.tap do |d|
- d.context = self
- end
- end
-
- def requested_resource
- @requested_resource ||= find_resource(params[:id]).tap do |resource|
- authorize_resource(resource)
- end
- end
-
- def find_resource(param)
- scoped_resource.find(param)
- end
-
- def scoped_resource
- resource_class.default_scoped
- end
-
- def apply_collection_includes(relation)
- resource_includes = dashboard.collection_includes
- return relation if resource_includes.empty?
- relation.includes(*resource_includes)
- end
-
- def resource_params
- params.require(resource_class.model_name.param_key)
- .permit(dashboard.permitted_attributes(action_name))
- .transform_values { |v| read_param_value(v) }
- end
-
- def read_param_value(data)
- if data.is_a?(ActionController::Parameters) && data[:type]
- if data[:type] == Administrate::Field::Polymorphic.to_s
- GlobalID::Locator.locate(data[:value])
- else
- raise "Unrecognised param data: #{data.inspect}"
+ if save_requested_resource
+ respond_to do |format|
+ format.html { respond_to_update_html }
end
- elsif data.is_a?(ActionController::Parameters)
- data.transform_values { |v| read_param_value(v) }
- elsif data.is_a?(String) && data.blank?
- nil
else
- data
+ respond_to do |format|
+ format.html { respond_to_update_error_html }
+ end
end
end
- delegate :dashboard_class, :resource_class, :resource_name, :namespace,
- to: :resource_resolver
- helper_method :namespace
- helper_method :resource_name
- helper_method :resource_class
-
- def resource_resolver
- @resource_resolver ||=
- Administrate::ResourceResolver.new(controller_path)
- end
-
- def translate_with_resource(key)
- t(
- "administrate.controller.#{key}",
- resource: resource_resolver.resource_title
- )
- end
-
- def show_search_bar?
- dashboard.attribute_types_for(
- dashboard.all_attributes
- ).any? { |_name, attribute| attribute.searchable? }
- end
-
- # Whether the current user is authorized to perform the named action on the
- # resource.
- #
- # @param _resource [ActiveRecord::Base, Class, String, Symbol] The
- # temptative target of the action, or the name of its class.
- # @param _action_name [String, Symbol] The name of an action that might be
- # possible to perform on a resource or resource class.
- # @return [Boolean] `true` if the current user is authorized to perform the
- # action on the resource. `false` otherwise.
- def authorized_action?(_resource, _action_name)
- true
- end
- helper_method :authorized_action?
-
- def new_resource(params = {})
- resource_class.new(params)
- end
- helper_method :new_resource
-
- def authorize_resource(resource)
- if authorized_action?(resource, action_name)
- resource
+ def destroy
+ if requested_resource.destroy
+ respond_to do |format|
+ format.html { respond_to_destroy_html }
+ end
else
- raise Administrate::NotAuthorizedError.new(
- action: action_name,
- resource: resource
- )
+ respond_to do |format|
+ format.html { respond_to_destroy_error_html }
+ end
end
end
-
- def paginate_resources(resources)
- resources.page(params[:_page]).per(records_per_page)
- end
end
end
diff --git a/app/controllers/concerns/administrate/authorizable.rb b/app/controllers/concerns/administrate/authorizable.rb
new file mode 100644
index 0000000000..9c7c395924
--- /dev/null
+++ b/app/controllers/concerns/administrate/authorizable.rb
@@ -0,0 +1,70 @@
+module Administrate
+ module Authorizable
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :existing_action?
+ helper_method :authorized_action?
+ end
+
+ private
+
+ # Whether the named action route exists for the resource class.
+ #
+ # @param resource [Class, String, Symbol] A class of resources, or the name
+ # of a class of resources.
+ # @param action_name [String, Symbol] The name of an action that might be
+ # possible to perform on a resource or resource class.
+ # @return [Boolean] `true` if a route exists for the resource class and the
+ # action. `false` otherwise.
+ def existing_action?(resource, action_name)
+ routes.include?([resource.to_s.underscore.pluralize, action_name.to_s])
+ end
+
+ # Whether the current user is authorized to perform the named action on the
+ # resource.
+ #
+ # @param _resource [ActiveRecord::Base, Class, String, Symbol] The
+ # tentative target of the action, or the name of its class.
+ # @param _action_name [String, Symbol] The name of an action that might be
+ # possible to perform on a resource or resource class.
+ # @return [Boolean] `true` if the current user is authorized to perform the
+ # action on the resource. `false` otherwise.
+ def authorized_action?(_resource, _action_name)
+ true
+ end
+
+ # Override this if you want to authorize the scope.
+ # This will be used in all actions except for the `new` and `create` actions.
+ #
+ # @param scope [ActiveRecord::Relation]
+ # @return [ActiveRecord::Relation]
+ def authorize_scope(scope)
+ scope
+ end
+
+ # Override this if you want to authorize the resource differently.
+ # This will be used to authorize the resource for all actions without `index`.
+ # In the case of `index`, it is used to authorize the resource class.
+ #
+ # @param resource [ActiveRecord::Base]
+ # @return [ActiveRecord::Base]
+ # @raise [Administrate::NotAuthorizedError] if the resource is not authorized.
+ def authorize_resource(resource)
+ if authorized_action?(resource, action_name)
+ resource
+ else
+ raise Administrate::NotAuthorizedError.new(
+ action: action_name,
+ resource: resource
+ )
+ end
+ end
+
+ protected
+
+ def routes
+ @routes ||= Namespace.new(namespace).routes.to_set
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/collection_paginator.rb b/app/controllers/concerns/administrate/collection_paginator.rb
new file mode 100644
index 0000000000..e16678bcf1
--- /dev/null
+++ b/app/controllers/concerns/administrate/collection_paginator.rb
@@ -0,0 +1,15 @@
+module Administrate
+ module CollectionPaginator
+ extend ActiveSupport::Concern
+
+ private
+
+ def records_per_page
+ params[:per_page] || 20
+ end
+
+ def paginate_resources(resources)
+ resources.page(params[:_page]).per(records_per_page)
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/collection_searchable.rb b/app/controllers/concerns/administrate/collection_searchable.rb
new file mode 100644
index 0000000000..7cf1bc6c5d
--- /dev/null
+++ b/app/controllers/concerns/administrate/collection_searchable.rb
@@ -0,0 +1,27 @@
+module Administrate
+ module CollectionSearchable
+ extend ActiveSupport::Concern
+
+ private
+
+ def filter_resources(resources)
+ Administrate::Search.new(
+ resources,
+ dashboard,
+ search_term
+ ).run
+ end
+
+ def search_term
+ @search_term ||= params[:search].to_s.strip
+ end
+
+ def filters
+ Administrate::Search.new(
+ scoped_resource,
+ dashboard,
+ search_term
+ ).valid_filters
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/collection_sortable.rb b/app/controllers/concerns/administrate/collection_sortable.rb
new file mode 100644
index 0000000000..7955e7b8c3
--- /dev/null
+++ b/app/controllers/concerns/administrate/collection_sortable.rb
@@ -0,0 +1,45 @@
+module Administrate
+ module CollectionSortable
+ extend ActiveSupport::Concern
+
+ private
+
+ def order
+ @order ||= Administrate::Order.new(
+ sorting_attribute,
+ sorting_direction,
+ sorting_column: sorting_column(
+ dashboard_attribute(sorting_attribute)
+ )
+ )
+ end
+
+ def sorting_column(dashboard_attribute)
+ return unless dashboard_attribute.try(:options)
+
+ dashboard_attribute.options.fetch(:sorting_column) {
+ dashboard_attribute.options.fetch(:order, nil)
+ }
+ end
+
+ def sorting_attribute
+ sorting_params.fetch(:order) { default_sorting_attribute }
+ end
+
+ def default_sorting_attribute
+ nil
+ end
+
+ def sorting_direction
+ sorting_params.fetch(:direction) { default_sorting_direction }
+ end
+
+ def default_sorting_direction
+ nil
+ end
+
+ def sorting_params
+ Hash.try_convert(request.query_parameters[resource_name]) || {}
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/controller_deprecator.rb b/app/controllers/concerns/administrate/controller_deprecator.rb
new file mode 100644
index 0000000000..c8594b3d82
--- /dev/null
+++ b/app/controllers/concerns/administrate/controller_deprecator.rb
@@ -0,0 +1,39 @@
+module Administrate
+ module ControllerDeprecator
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :valid_action?
+ helper_method :show_action?
+ helper_method :nav_link_state
+ end
+
+ private
+
+ # @deprecated Use {#existing_action} instead. Note that, in
+ # {#existing_action}, the order of parameters is reversed and there is
+ # no default value for the `resource` parameter.
+ def valid_action?(action_name, resource = resource_class)
+ Administrate.warn_of_deprecated_authorization_method(__method__)
+ existing_action?(resource, action_name)
+ end
+
+ # @deprecated Use {#authorized_action} instead. Note that the order of
+ # parameters is reversed in {#authorized_action}.
+ def show_action?(action, resource)
+ Administrate.warn_of_deprecated_authorization_method(__method__)
+ authorized_action?(resource, action)
+ end
+
+ def nav_link_state(resource)
+ underscore_resource = resource.to_s.split("/").join("__")
+ (resource_name.to_s.pluralize == underscore_resource) ? :active : :inactive
+ end
+
+ def show_search_bar?
+ dashboard.attribute_types_for(
+ dashboard.all_attributes
+ ).any? { |_name, attribute| attribute.searchable? }
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/controller_i18n.rb b/app/controllers/concerns/administrate/controller_i18n.rb
new file mode 100644
index 0000000000..027d76c294
--- /dev/null
+++ b/app/controllers/concerns/administrate/controller_i18n.rb
@@ -0,0 +1,14 @@
+module Administrate
+ module ControllerI18n
+ extend ActiveSupport::Concern
+
+ private
+
+ def translate_with_resource(key)
+ t(
+ "administrate.controller.#{key}",
+ resource: resource_resolver.resource_title
+ )
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/dashboard_manager.rb b/app/controllers/concerns/administrate/dashboard_manager.rb
new file mode 100644
index 0000000000..9d12360248
--- /dev/null
+++ b/app/controllers/concerns/administrate/dashboard_manager.rb
@@ -0,0 +1,30 @@
+module Administrate
+ module DashboardManager
+ extend ActiveSupport::Concern
+
+ included do
+ delegate :dashboard_class, :resource_class, :resource_name, :namespace,
+ to: :resource_resolver
+ helper_method :namespace
+ helper_method :resource_name
+ helper_method :resource_class
+ end
+
+ private
+
+ def dashboard
+ @dashboard ||= dashboard_class.new.tap do |d|
+ d.context = self
+ end
+ end
+
+ def dashboard_attribute(attribute)
+ dashboard.attribute_types[attribute.to_sym] if attribute
+ end
+
+ def resource_resolver
+ @resource_resolver ||=
+ Administrate::ResourceResolver.new(controller_path)
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/html_renderer.rb b/app/controllers/concerns/administrate/html_renderer.rb
new file mode 100644
index 0000000000..5827c3fbc5
--- /dev/null
+++ b/app/controllers/concerns/administrate/html_renderer.rb
@@ -0,0 +1,109 @@
+module Administrate
+ module HtmlRenderer
+ extend ActiveSupport::Concern
+
+ private
+
+ def respond_to_index_html
+ render locals: {
+ resources: collection_resources,
+ search_term: search_term,
+ page: index_page,
+ show_search_bar: show_search_bar?,
+ filters: filters
+ }
+ end
+
+ def respond_to_show_html
+ render locals: {
+ page: show_page
+ }
+ end
+
+ def respond_to_new_html
+ render locals: {
+ page: new_page
+ }
+ end
+
+ def respond_to_edit_html
+ render locals: {
+ page: edit_page
+ }
+ end
+
+ def respond_to_create_html
+ redirect_to(
+ after_resource_created_path(built_resource),
+ notice: translate_with_resource("create.success")
+ )
+ end
+
+ def respond_to_create_error_html
+ render :new, locals: {
+ page: new_page
+ }, status: :unprocessable_entity
+ end
+
+ def respond_to_update_html
+ redirect_to(
+ after_resource_updated_path(requested_resource),
+ notice: translate_with_resource("update.success"),
+ status: :see_other
+ )
+ end
+
+ def respond_to_update_error_html
+ render :edit, locals: {
+ page: edit_page
+ }, status: :unprocessable_entity
+ end
+
+ def respond_to_destroy_html
+ flash[:notice] = translate_with_resource("destroy.success")
+ redirect_to after_resource_destroyed_path(requested_resource), status: :see_other
+ end
+
+ def respond_to_destroy_error_html
+ flash[:error] = requested_resource.errors.full_messages.join("
")
+ redirect_to after_resource_destroyed_path(requested_resource), status: :see_other
+ end
+
+ def index_page
+ page = Administrate::Page::Collection.new(dashboard, order: order)
+ page.context = self
+ collection_resources
+ page
+ end
+
+ def show_page
+ page = Administrate::Page::Show.new(dashboard, requested_resource)
+ page.context = self
+ page
+ end
+
+ def new_page
+ page = Administrate::Page::Form.new(dashboard, built_resource)
+ page.context = self
+ page
+ end
+
+ def edit_page
+ page = Administrate::Page::Form.new(dashboard, requested_resource)
+ page.context = self
+ page
+ end
+
+ def after_resource_destroyed_path(_requested_resource)
+ {action: :index}
+ end
+
+ def after_resource_created_path(requested_resource)
+ [namespace, requested_resource]
+ end
+
+ def after_resource_updated_path(requested_resource)
+ [namespace, requested_resource]
+ end
+ end
+end
diff --git a/app/controllers/concerns/administrate/resource_manager.rb b/app/controllers/concerns/administrate/resource_manager.rb
new file mode 100644
index 0000000000..92407fb878
--- /dev/null
+++ b/app/controllers/concerns/administrate/resource_manager.rb
@@ -0,0 +1,128 @@
+module Administrate
+ module ResourceManager
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :new_resource
+ end
+
+ private
+
+ def collection_resources
+ @collection_resources ||= begin
+ authorize_resource(resource_class)
+ resources = authorize_scope(scoped_resource)
+ resources = filter_resources(resources)
+ resources = apply_collection_includes(resources)
+ resources = order.apply(resources)
+ resources = paginate_resources(resources)
+ @resources = resources
+ resources
+ end
+ end
+
+ def built_resource
+ @built_resource ||= new_resource(resource_params).tap do |resource|
+ authorize_resource(resource)
+ @resource = resource
+ end
+ end
+
+ def requested_resource
+ @requested_resource ||= find_resource(params[:id]).tap do |resource|
+ authorize_resource(resource)
+ @resource = resource
+ end
+ end
+
+ # Override this if you have certain roles that require a subset.
+ # This will be used in all actions except for the `new` and `create` actions.
+ #
+ # @return [ActiveRecord::Relation]
+ def scoped_resource
+ resource_class.default_scoped
+ end
+
+ def new_resource(params = {})
+ resource_class.new(params)
+ end
+
+ # Override this method to specify custom lookup behavior.
+ # This will be used to set the resource for the `show`, `edit`, `update` and `destroy` actions.
+ #
+ # @param param [ActiveSupport::Parameter]
+ # @return [ActiveRecord::Base]
+ def find_resource(param)
+ authorize_scope(scoped_resource).find(param)
+ end
+
+ def save_built_resource
+ built_resource.save(context: validation_contexts_on_create(built_resource))
+ end
+
+ def save_built_resource!
+ built_resource.save!(context: validation_contexts_on_create(built_resource))
+ end
+
+ def save_requested_resource
+ requested_resource.save(context: validation_contexts_on_update(requested_resource))
+ end
+
+ def save_requested_resource!
+ requested_resource.save!(context: validation_contexts_on_update(requested_resource))
+ end
+
+ # Override this if you want to provide additional validation contexts.
+ #
+ # @param resource [ActiveRecord::Base] The resource to be validated.
+ # @return [Array] The validation contexts to be used.
+ def validation_contexts_on_create(resource)
+ default_validation_contexts(resource)
+ end
+
+ # Override this if you want to provide additional validation contexts.
+ #
+ # @param resource [ActiveRecord::Base] The resource to be validated.
+ # @return [Array] The validation contexts to be used.
+ def validation_contexts_on_update(resource)
+ default_validation_contexts(resource)
+ end
+
+ def default_validation_contexts(resource)
+ nil
+ end
+
+ def resource_params
+ attributes = params.fetch(resource_class.model_name.param_key, {})
+ return {} if attributes.empty?
+
+ attributes
+ .permit(dashboard.permitted_attributes(action_name))
+ .transform_values { |v| read_param_value(v) }
+ end
+
+ protected
+
+ def apply_collection_includes(relation)
+ resource_includes = dashboard.collection_includes
+ return relation if resource_includes.empty?
+ relation.includes(*resource_includes)
+ end
+
+ def read_param_value(data)
+ if data.is_a?(ActionController::Parameters) && data[:type]
+ if data[:type] == Administrate::Field::Polymorphic.to_s
+ GlobalID::Locator.locate(data[:value])
+ else
+ raise "Unrecognised param data: #{data.inspect}"
+ end
+ elsif data.is_a?(ActionController::Parameters)
+ data.transform_values { |v| read_param_value(v) }
+ elsif data.is_a?(String) && data.blank?
+ nil
+ else
+ data
+ end
+ end
+ end
+end
diff --git a/spec/example_app/app/controllers/admin/log_entries_controller.rb b/spec/example_app/app/controllers/admin/log_entries_controller.rb
index ae1705956b..c23e224aa0 100644
--- a/spec/example_app/app/controllers/admin/log_entries_controller.rb
+++ b/spec/example_app/app/controllers/admin/log_entries_controller.rb
@@ -1,6 +1,6 @@
module Admin
class LogEntriesController < Admin::ApplicationController
- def filter_resources(resources, search_term:)
+ def filter_resources(resources)
return resources if search_term.blank?
customer_ids = Customer.where(