app/controllers/avo/base_controller.rb
require_dependency "avo/application_controller"
module Avo
class BaseController < ApplicationController
include Avo::Concerns::FiltersSessionHandler
before_action :set_resource_name
before_action :set_resource
before_action :set_applied_filters, only: :index
before_action :set_record, only: [:show, :edit, :destroy, :update, :preview]
before_action :set_record_to_fill, only: [:new, :edit, :create, :update]
before_action :detect_fields
before_action :set_edit_title_and_breadcrumbs, only: [:edit, :update]
before_action :fill_record, only: [:create, :update]
# Don't run base authorizations for associations
before_action :authorize_base_action, except: :preview, if: -> { controller_name != "associations" }
before_action :set_pagy_locale, only: :index
def index
@page_title = @resource.plural_name.humanize
add_breadcrumb @resource.plural_name.humanize
set_index_params
set_filters
set_actions
# If we don't get a query object predefined from a child controller like associations, just spin one up
unless defined? @query
@query = @resource.class.query_scope
end
# Eager load the associations
if @resource.includes.present?
@query = @query.includes(*@resource.includes)
end
# Sort the items
if @index_params[:sort_by].present?
unless @index_params[:sort_by].eql? :created_at
@query = @query.unscope(:order)
end
# Check if the sortable field option is actually a proc and we need to do a custom sort
field_id = @index_params[:sort_by].to_sym
field = @resource.get_field_definitions.find { |field| field.id == field_id }
@query = if field&.sortable.is_a?(Proc)
Avo::ExecutionContext.new(target: field.sortable, query: @query, direction: @index_params[:sort_direction]).handle
else
@query.order("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}")
end
end
# Apply filters to the current query
filters_to_be_applied.each do |filter_class, filter_value|
@query = filter_class.safe_constantize.new(
arguments: @resource.get_filter_arguments(filter_class)
).apply_query request, @query, filter_value
end
safe_call :set_and_apply_scopes
safe_call :apply_dynamic_filters
apply_pagination
# Create resources for each record
# Duplicate the @resource before hydration to avoid @resource keeping last record.
@resource.hydrate(params: params)
@resources = @records.map do |record|
@resource.dup.hydrate(record: record)
end
set_component_for __method__
end
def show
@resource.hydrate(record: @record, view: :show, user: _current_user, params: params).detect_fields
set_actions
@page_title = @resource.default_panel_name.to_s
# If we're accessing this resource via another resource add the parent to the breadcrumbs.
if params[:via_resource_class].present? && params[:via_record_id].present?
via_resource = Avo.resource_manager.get_resource(params[:via_resource_class])
via_record = via_resource.find_record params[:via_record_id], params: params
via_resource = via_resource.new record: via_record
add_breadcrumb via_resource.plural_name, resources_path(resource: via_resource)
add_breadcrumb via_resource.record_title, resource_path(record: via_record, resource: via_resource)
end
add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
add_breadcrumb @resource.record_title
add_breadcrumb I18n.t("avo.details").upcase_first
set_component_for __method__
end
def new
# Record is already hydrated on set_record_to_fill method
@record = @resource.record
@resource.hydrate(view: :new, user: _current_user)
# Handle special cases when creating a new record via a belongs_to relationship
if params[:via_belongs_to_resource_class].present?
return render turbo_stream: turbo_stream.append("attach_modal", partial: "avo/base/new_via_belongs_to")
end
set_actions
@page_title = @resource.default_panel_name.to_s
if is_associated_record?
via_resource = Avo.resource_manager.get_resource_by_model_class(params[:via_relation_class])
via_record = via_resource.find_record params[:via_record_id], params: params
via_resource = via_resource.new record: via_record
add_breadcrumb via_resource.plural_name, resources_path(resource: via_resource)
add_breadcrumb via_resource.record_title, resource_path(record: via_record, resource: via_resource)
end
add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
add_breadcrumb t("avo.new").humanize
set_component_for __method__, fallback_view: :edit
end
def create
# This means that the record has been created through another parent record and we need to attach it somehow.
if params[:via_record_id].present? && params[:via_belongs_to_resource_class].nil?
@reflection = @record._reflections[params[:via_relation]]
# Figure out what kind of association does the record have with the parent record
# Fills in the required info for belongs_to and has_many
# Get the foreign key and set it to the id we received in the params
if @reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection) || @reflection.is_a?(ActiveRecord::Reflection::HasManyReflection)
related_resource = Avo.resource_manager.get_resource_by_model_class params[:via_relation_class]
related_record = related_resource.find_record params[:via_record_id], params: params
@record.send(:"#{@reflection.foreign_key}=", related_record.id)
end
# For when working with has_one, has_one_through, has_many_through, has_and_belongs_to_many, polymorphic
if @reflection.is_a? ActiveRecord::Reflection::ThroughReflection
# find the record
via_resource = Avo.resource_manager.get_resource_by_model_class(params[:via_relation_class])
@related_record = via_resource.find_record params[:via_record_id], params: params
association_name = BaseResource.valid_association_name(@record, params[:via_relation])
if params[:via_association_type] == "has_one"
# On has_one scenarios we should switch the @record and @related_record
@related_record.send(:"#{@reflection.parent_reflection.inverse_of.name}=", @record)
else
@record.send(association_name) << @related_record
end
end
end
# record gets instantiated and filled in the fill_record method
saved = save_record
@resource.hydrate(record: @record, view: :new, user: _current_user)
add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
add_breadcrumb t("avo.new").humanize
set_actions
set_component_for :edit
if saved
create_success_action
else
create_fail_action
end
end
def edit
set_actions
set_component_for __method__
end
def update
# record gets instantiated and filled in the fill_record method
saved = save_record
@resource = @resource.hydrate(record: @record, view: :edit, user: _current_user)
set_actions
set_component_for :edit
if saved
update_success_action
else
update_fail_action
end
end
def destroy
if destroy_model
destroy_success_action
else
destroy_fail_action
end
end
def preview
@resource.hydrate(record: @record, view: :show, user: _current_user, params: params)
render layout: params[:turbo_frame].blank?
end
private
def save_record
perform_action_and_record_errors do
save_record_action
end
end
def save_record_action
@record.save!
end
def destroy_model
perform_action_and_record_errors do
destroy_record_action
end
end
def destroy_record_action
@record.destroy!
end
def perform_action_and_record_errors(&block)
begin
succeeded = block.call
rescue => exception
# In case there's an error somewhere else than the record
# Example: When you save a license that should create a user for it and creating that user throws and error.
# Example: When you Try to delete a record and has a foreign key constraint.
exception_message = exception.message
end
# Add the errors from the record
@errors = @record.errors.full_messages
# Remove duplicated errors
if exception_message.present?
@errors = @errors.reject { |error| exception_message.include? error }.unshift exception_message
end
@errors.any? ? false : succeeded
end
def model_params
request_params = params.require(model_param_key).permit(permitted_params)
if @resource.devise_password_optional && request_params[:password].blank? && request_params[:password_confirmation].blank?
request_params.delete(:password_confirmation)
request_params.delete(:password)
end
request_params
end
def permitted_params
@resource.get_field_definitions.select(&:updatable).map(&:to_permitted_param).concat(extra_params).uniq
end
def extra_params
@resource.class.extra_params || []
end
def cast_nullable(params)
fields = @resource.get_field_definitions
nullable_fields = fields
.filter do |field|
field.nullable
end
.map do |field|
[field.id, field.null_values]
end
.to_h
params.each do |key, value|
nullable_values = nullable_fields[key.to_sym]
if nullable_values.present? && value.in?(nullable_values)
params[key] = nil
end
end
params
end
def set_index_params
@index_params = {}
# Pagination
@index_params[:page] = params[:page] || 1
@index_params[:per_page] = Avo.configuration.per_page
if cookies[:per_page].present?
@index_params[:per_page] = cookies[:per_page]
end
if @parent_record.present?
@index_params[:per_page] = Avo.configuration.via_per_page
end
if params[:per_page].present?
@index_params[:per_page] = params[:per_page]
cookies[:per_page] = params[:per_page]
end
# Sorting
if params[:sort_by].present?
@index_params[:sort_by] = params[:sort_by]
elsif @resource.model_class.present? && @resource.model_class.column_names.include?("created_at")
@index_params[:sort_by] = :created_at
end
@index_params[:sort_direction] = params[:sort_direction] || :desc
# View types
available_view_types = @resource.available_view_types
@index_params[:available_view_types] = available_view_types
@index_params[:view_type] = if params[:view_type].present?
params[:view_type]
elsif available_view_types.size == 1
available_view_types.first
else
Avo::ExecutionContext.new(
target: @resource.default_view_type || Avo.configuration.default_view_type,
resource: @resource,
view: @view
).handle
end
if available_view_types.exclude? @index_params[:view_type].to_sym
raise "View type '#{@index_params[:view_type]}' is unavailable for #{@resource.class}."
end
end
def set_filters
@filters = @resource
.get_filters
.map do |filter|
filter[:class].new arguments: filter[:arguments]
end
.select do |filter|
filter.visible_in_view(resource: @resource, parent_resource: @parent_resource)
end
end
def set_actions
@actions = @resource
.get_actions
.map do |action_bag|
action_bag.delete(:class).new(record: @record, resource: @resource, view: @view, **action_bag)
end
.select do |action|
action.is_a?(DividerComponent) || action.visible_in_view(parent_resource: @parent_resource)
end
end
def set_applied_filters
reset_filters if params[:reset_filter]
# Return if there are no filters or if the filters are actually ActionController::Parameters (used by dynamic filters)
return @applied_filters = {} if (fetched_filters = fetch_filters).blank? || fetched_filters.is_a?(ActionController::Parameters)
@applied_filters = Avo::Filters::BaseFilter.decode_filters(fetched_filters)
# Some filters react to others and will have to be merged into this
@applied_filters = @applied_filters.merge reactive_filters
end
def reactive_filters
filter_reactions = {}
# Go through all filters
@resource.get_filters
.select do |filter|
filter[:class].instance_methods(false).include? :react
end
.each do |filter|
# Run the react method if it's present
reaction = filter[:class].new(arguments: filter[:arguments]).react
next if reaction.nil?
filter_reactions[filter[:class].to_s] = reaction
end
filter_reactions
end
# Get the default state of the filters and override with the user applied filters
def filters_to_be_applied
filter_defaults = {}
@resource.get_filters.each do |filter|
filter = filter[:class].new arguments: filter[:arguments]
unless filter.default.nil?
filter_defaults[filter.class.to_s] = filter.default
end
end
filter_defaults.merge(@applied_filters)
end
def set_edit_title_and_breadcrumbs
@resource = @resource.hydrate(record: @record, view: :edit, user: _current_user)
@page_title = @resource.default_panel_name.to_s
last_crumb_args = {}
# If we're accessing this resource via another resource add the parent to the breadcrumbs.
if params[:via_resource_class].present? && params[:via_record_id].present?
via_resource = Avo.resource_manager.get_resource(params[:via_resource_class])
via_record = via_resource.find_record params[:via_record_id], params: params
via_resource = via_resource.new record: via_record
add_breadcrumb via_resource.plural_name, resources_path(resource: @resource)
add_breadcrumb via_resource.record_title, resource_path(record: via_record, resource: via_resource)
last_crumb_args = {
via_resource_class: params[:via_resource_class],
via_record_id: params[:via_record_id]
}
else
add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
end
add_breadcrumb @resource.record_title, resource_path(record: @resource.record, resource: @resource, **last_crumb_args)
add_breadcrumb t("avo.edit").humanize
end
def create_success_action
return render "close_modal_and_reload_field" if params[:via_belongs_to_resource_class].present?
respond_to do |format|
format.html { redirect_to after_create_path, notice: create_success_message }
end
end
def create_fail_action
flash.now[:error] = create_fail_message
respond_to do |format|
format.html { render :new, status: :unprocessable_entity }
format.turbo_stream { render "create_fail_action" }
end
end
def create_success_message
"#{@resource.name} #{t("avo.was_successfully_created")}."
end
def create_fail_message
t "avo.you_missed_something_check_form"
end
def after_create_path
# If this is an associated record return to the association show page
if is_associated_record?
parent_resource = if params[:via_resource_class].present?
Avo.resource_manager.get_resource(params[:via_resource_class])
else
Avo.resource_manager.get_resource_by_model_class(params[:via_relation_class])
end
association_name = BaseResource.valid_association_name(@record, params[:via_relation])
return resource_view_path(
record: @record.send(association_name),
resource: parent_resource,
resource_id: params[:via_record_id]
)
end
redirect_path_from_resource_option(:after_create_path) || resource_view_response_path
end
def update_success_action
respond_to do |format|
format.html { redirect_to after_update_path, notice: update_success_message }
end
end
def update_fail_action
respond_to do |format|
flash.now[:error] = update_fail_message
format.html { render :edit, status: :unprocessable_entity }
end
end
def update_success_message
"#{@resource.name} #{t("avo.was_successfully_updated")}."
end
def update_fail_message
t "avo.you_missed_something_check_form"
end
def after_update_path
return params[:referrer] if params[:referrer].present?
redirect_path_from_resource_option(:after_update_path) || resource_view_response_path
end
# Requires a different/special name, otherwise, in some places, this can be called instead helpers.resource_view_path
def resource_view_response_path
helpers.resource_view_path(record: @record, resource: @resource)
end
def destroy_success_action
respond_to do |format|
format.html { redirect_to after_destroy_path, notice: destroy_success_message }
end
end
def destroy_fail_action
flash[:error] = destroy_fail_message
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.flash_alerts }
end
end
def destroy_success_message
t("avo.resource_destroyed", attachment_class: @attachment_class)
end
def destroy_fail_message
@errors.present? ? @errors.join(". ") : t("avo.failed")
end
def after_destroy_path
params[:referrer] || resources_path(resource: @resource, turbo_frame: params[:turbo_frame], view_type: params[:view_type])
end
def redirect_path_from_resource_option(action = :after_update_path)
return nil if @resource.class.send(action).blank?
if @resource.class.send(action) == :index
resources_path(resource: @resource)
elsif @resource.class.send(action) == :edit || Avo.configuration.resource_default_view.edit?
edit_resource_path(resource: @resource, record: @resource.record)
else
resource_path(record: @record, resource: @resource)
end
end
def is_associated_record?
params[:via_relation_class].present? && params[:via_record_id].present?
end
# Set pagy locale from params or from avo configuration, if both nil locale = "en"
def set_pagy_locale
@pagy_locale = locale.to_s || Avo.configuration.locale || "en"
end
def safe_call(method)
send(method) if respond_to?(method, true)
end
def pagy_query
@query
end
# Set the view component for the current view
# It will try to use the custom component if it's set, otherwise it will use the default one
def set_component_for(view, fallback_view: nil)
# Fetch the components from the resource
components = Avo::ExecutionContext.new(
target: @resource.components,
resource: @resource,
record: @record,
view: @view
).handle
# If the component is not set, use the default one
if (custom_component = components.dig(:"resource_#{view}_component")).nil?
return @component = "Avo::Views::Resource#{(fallback_view || view).to_s.classify}Component".constantize
end
# If the component is set, try to use it
@component = custom_component.to_s.safe_constantize
# If the component is not found, raise an error
if @component.nil?
raise "The component '#{custom_component}' was not found.\n" \
"That component was fetched from 'self.components' option inside '#{@resource.class}' resource."
end
end
def apply_pagination
@pagy, @records = @resource.apply_pagination(index_params: @index_params, query: pagy_query)
end
end
end