app/controllers/avo/associations_controller.rb
require_dependency "avo/base_controller"
module Avo
class AssociationsController < BaseController
before_action :set_record, only: [:show, :index, :new, :create, :destroy]
before_action :set_related_resource_name
before_action :set_related_resource, only: [:show, :index, :new, :create, :destroy]
before_action :set_related_authorization
before_action :set_reflection_field
before_action :set_related_record, only: [:show]
before_action :set_reflection
before_action :set_attachment_class, only: [:show, :index, :new, :create, :destroy]
before_action :set_attachment_resource, only: [:show, :index, :new, :create, :destroy]
before_action :set_attachment_record, only: [:create, :destroy]
before_action :set_attach_fields, only: [:new, :create]
before_action :authorize_index_action, only: :index
before_action :authorize_attach_action, only: :new
before_action :authorize_detach_action, only: :destroy
layout :choose_layout
def index
@parent_resource = @resource.dup
@resource = @related_resource
@parent_record = @parent_resource.find_record(params[:id], params: params)
@parent_resource.hydrate(record: @parent_record)
association_name = BaseResource.valid_association_name(@parent_record, association_from_params)
@query = @related_authorization.apply_policy @parent_record.send(association_name)
@association_field = find_association_field(resource: @parent_resource, association: params[:related_name])
if @association_field.present? && @association_field.scope.present?
@query = Avo::ExecutionContext.new(
target: @association_field.scope,
query: @query,
parent: @parent_record,
resource: @resource,
parent_resource: @parent_resource
).handle
end
super
end
def show
@parent_resource, @parent_record = @resource, @record
@resource, @record = @related_resource, @related_record
super
end
def new
@resource.hydrate(record: @record)
if @field.present? && !@field.is_searchable?
query = @related_authorization.apply_policy @attachment_class
# Add the association scope to the query scope
if @field.attach_scope.present?
query = Avo::ExecutionContext.new(target: @field.attach_scope, query: query, parent: @record).handle
end
@options = select_options(query)
end
@url = Avo::Services::URIService.parse(avo.root_url.to_s)
.append_paths("resources", params[:resource_name], params[:id], params[:related_name])
.append_query(
{
view: @resource&.view&.to_s,
for_attribute: @field&.try(:for_attribute)
}.compact
)
.to_s
end
def create
if create_association
create_success_action
else
create_fail_action
end
end
def create_association
association_name = BaseResource.valid_association_name(@record, association_from_params)
perform_action_and_record_errors do
if through_reflection? && additional_params.present?
new_join_record.save
elsif has_many_reflection? || through_reflection?
@record.send(association_name) << @attachment_record
else
@record.send(:"#{association_name}=", @attachment_record)
@record.save!
end
end
end
def destroy
association_name = BaseResource.valid_association_name(@record, @field.for_attribute || params[:related_name])
if through_reflection?
join_record.destroy!
elsif has_many_reflection?
@record.send(association_name).delete @attachment_record
else
@record.send(:"#{association_name}=", nil)
end
destroy_success_action
end
private
def set_reflection
@reflection = @record.class.reflect_on_association(association_from_params)
# Ensure inverse_of is present on STI
if !@record.class.descends_from_active_record? && @reflection.inverse_of.blank? && Rails.env.development?
raise "Avo relies on the 'inverse_of' option to establish the inverse association and perform some specific logic.\n" \
"Please configure the 'inverse_of' option for the '#{@reflection.macro} :#{@reflection.name}' association " \
"in the '#{@reflection.active_record.name}' model."
end
end
def set_attachment_class
@attachment_class = @reflection.klass
end
def set_attachment_resource
@attachment_resource = @field.use_resource || (Avo.resource_manager.get_resource_by_model_class @attachment_class)
end
def set_attachment_record
@attachment_record = @related_resource.find_record attachment_id, params: params
end
def set_reflection_field
@field = find_association_field(resource: @resource, association: @related_resource_name)
@field.hydrate(resource: @resource, record: @record, view: Avo::ViewInquirer.new(:new))
rescue
end
def attachment_id
params[:related_id] || params.dig(:fields, :related_id)
end
def reflection_class
if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
@reflection.through_reflection.class
else
@reflection.class
end
end
def authorize_if_defined(method, record = @record)
@authorization.set_record(record)
if @authorization.has_method?(method.to_sym)
@authorization.authorize_action method.to_sym
elsif !@authorization.is_a?(Avo::Services::AuthorizationService) && Avo.configuration.explicit_authorization
raise Avo::NotAuthorizedError.new
end
end
def authorize_index_action
authorize_if_defined "view_#{@field.id}?"
end
def authorize_attach_action
authorize_if_defined "attach_#{@field.id}?"
end
def authorize_detach_action
authorize_if_defined "detach_#{@field.id}?", @attachment_record
end
def set_related_authorization
@related_authorization = if @related_resource.present?
@related_resource.authorization(user: _current_user)
else
Services::AuthorizationService.new _current_user
end
end
def association_from_params
@field&.for_attribute || params[:related_name]
end
def source_foreign_key
@reflection.source_reflection.foreign_key
end
def through_foreign_key
@reflection.through_reflection.foreign_key
end
def join_record
@reflection.through_reflection.klass.find_by(source_foreign_key => @attachment_record.id,
through_foreign_key => @record.id)
end
def has_many_reflection?
reflection_class.in? [
ActiveRecord::Reflection::HasManyReflection,
ActiveRecord::Reflection::HasAndBelongsToManyReflection
]
end
def through_reflection?
@reflection.instance_of? ActiveRecord::Reflection::ThroughReflection
end
def additional_params
@additional_params ||= params[:fields].slice(*@attach_fields&.map(&:id))
end
def set_attach_fields
@attach_fields = if @field.attach_fields.present?
Avo::FieldsExecutionContext.new(target: @field.attach_fields)
.detect_fields
.items_holder
.items
end
end
def new_join_record
@resource.fill_record(
@reflection.through_reflection.klass.new(
source_foreign_key => @attachment_record.id,
through_foreign_key => @record.id
),
additional_params,
fields: @attach_fields,
)
end
def create_success_action
flash[:notice] = t("avo.attachment_class_attached", attachment_class: @related_resource.name)
respond_to do |format|
if params[:turbo_frame].present?
format.turbo_stream { render turbo_stream: reload_frame_turbo_streams }
else
format.html { redirect_back fallback_location: resource_view_response_path }
end
end
end
def reload_frame_turbo_streams
turbo_streams = super
# We want to close the modal if the user wants to add just one record
turbo_streams << turbo_stream.close_modal if params[:button] != "attach_another"
turbo_streams
end
def create_fail_action
flash[:error] = t("avo.attachment_failed", attachment_class: @related_resource.name)
respond_to do |format|
format.turbo_stream {
render turbo_stream: turbo_stream.append("alerts", partial: "avo/partials/all_alerts")
}
end
end
def destroy_success_action
flash[:notice] = t("avo.attachment_class_detached", attachment_class: @attachment_class)
respond_to do |format|
if params[:turbo_frame].present?
format.turbo_stream do
render turbo_stream: reload_frame_turbo_streams
end
else
format.html { redirect_to params[:referrer] || resource_view_response_path }
end
end
end
def select_options(query)
query.all.limit(Avo.configuration.associations_lookup_list_limit).map do |record|
[@attachment_resource.new(record: record).record_title, record.to_param]
end.tap do |options|
options << t("avo.more_records_available") if options.size == Avo.configuration.associations_lookup_list_limit
end
end
end
end