module CmAdmin
class ResourceController < ApplicationController
include Pundit::Authorization
include Pagy::Backend
helper CmAdmin::ViewHelpers
skip_before_action :verify_authenticity_token, only: :reset_sort_columns
def cm_index(params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'index')
# Based on the params the filter and pagination object to be set
authorize @ar_object, policy_class: "CmAdmin::#{controller_name.classify}Policy".constantize if defined? "CmAdmin::#{controller_name.classify}Policy".constantize
records = "CmAdmin::#{@model.name}Policy::IndexScope".constantize.new(Current.user, @model.name.constantize).resolve
records = apply_scopes(records)
@ar_object = if %w[table card].include?(params[:view_type]) || %i[table card].include?(@current_action.view_type)
filter_by(params, records, filter_params: @model.filter_params(params))
elsif (request.xhr? && params[:view_type] == 'kanban') || @current_action.view_type == :kanban
kanban_filter_by(params, records, @model.filter_params(params))
else
filter_by(params, records, filter_params: @model.filter_params(params))
end
respond_to do |format|
if request.xhr? && (params[:view_type] == 'kanban' || @current_action.view_type == :kanban)
format.json { render json: @ar_object }
elsif request.xhr?
format.html { render partial: '/cm_admin/main/table' }
else
format.html { render '/cm_admin/main/' + action_name }
end
end
end
def cm_show(params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'show')
scoped_model = "CmAdmin::#{@model.name}Policy::ShowScope".constantize.new(Current.user, @model.name.constantize).resolve
@ar_object = fetch_ar_object(scoped_model, params[:id])
@alerts = @model.alerts
resource_identifier
respond_to do |format|
if request.xhr?
format.html { render partial: '/cm_admin/main/show_content', locals: { via_xhr: true } }
else
format.html { render '/cm_admin/main/' + action_name }
end
format.json { render json: @ar_object.to_builder.target! }
end
end
def cm_new(_params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'new')
@ar_object = @model.ar_model.new
resource_identifier
respond_to do |format|
format.html { render '/cm_admin/main/' + action_name }
end
end
def cm_edit(params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'edit')
@ar_object = fetch_ar_object(@model.ar_model.name.classify.constantize, params[:id])
resource_identifier
respond_to do |format|
format.html { render '/cm_admin/main/' + action_name }
end
end
def cm_update(params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'edit')
@ar_object = fetch_ar_object(@model.ar_model.name.classify.constantize, params[:id])
@ar_object.assign_attributes(resource_params(params))
resource_identifier
resource_responder
end
def cm_create(params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'new')
@ar_object = @model.ar_model.name.classify.constantize.new(resource_params(params))
resource_identifier
resource_responder
end
def cm_destroy(params)
@ar_object = fetch_ar_object(@model.ar_model.name.classify.constantize, params[:id])
redirect_url = request.referrer || cm_admin.send("#{@model.name.underscore}_index_path")
respond_to do |format|
if @ar_object.destroy
format.html { redirect_back fallback_location: redirect_url, notice: "#{@model.formatted_name} was deleted" }
else
format.html { redirect_back fallback_location: redirect_url, alert: "#{@model.formatted_name} could not be deleted" }
end
end
end
def import
@model = Model.find_by({ name: controller_name.classify })
allowed_params = params.permit(file_import: %i[associated_model_name import_file]).to_h
file_import = ::FileImport.new(allowed_params[:file_import])
file_import.added_by = Current.user
respond_to do |format|
format.html { redirect_back fallback_location: cm_admin.send("#{@model.name.underscore}_index_path"), notice: 'Your import is successfully queued.' } if file_import.save!
end
end
def import_form
@model = Model.find_by({ name: controller_name.classify })
respond_to do |format|
format.html { render '/cm_admin/main/import_form' }
end
end
def cm_bulk_action(params)
@model = Model.find_by({ name: controller_name.classify })
@bulk_action_processor = CmAdmin::BulkActionProcessor.new(@action, @model, params).perform_bulk_action
respond_to do |format|
if @bulk_action_processor.invalid_records.empty?
flash[:notice] = "#{@action.formatted_name} was successful"
flash[:bulk_action_success] = "#{@action.formatted_name} was successful"
format.html { redirect_to request.referrer }
else
error_messages = @bulk_action_processor.invalid_records.map do |invalid_record|
"
#{invalid_record.error_message}"
end.join
flash[:alert] = "#{@action.formatted_name} was unsuccessful"
flash[:bulk_action_error] = "
#{@action.formatted_name} encountered the following errors:
"
format.html { redirect_to request.referrer }
end
end
end
def cm_history(_params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: 'history')
resource_identifier
respond_to do |format|
format.html { render '/cm_admin/main/history' }
end
end
def cm_custom_method(params)
records = "CmAdmin::#{@model.name}Policy::#{@action.name.classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve
@current_action = @action
if @action.parent == 'index'
records = apply_scopes(records)
@ar_object = filter_by(params, records, filter_params: @model.filter_params(params))
else
resource_identifier
end
respond_to do |format|
if @action.action_type == :custom
if @action.child_records
if request.xhr?
format.html { render partial: '/cm_admin/main/associated_table' }
else
format.html { render @action.layout }
end
elsif @action.display_type == :page
@action.parent == 'index' ? @ar_object.data : @ar_object
# TODO: To set a default value for @action.layout, Since it is used in render above,
# Need to check and fix it.
format.html { render @action.partial, layout: @action.layout || 'cm_admin' }
else
begin
response_object = @action.code_block.call(@response_object)
if response_object.class == Hash
format.json { render json: response_object }
elsif response_object.errors.empty?
redirect_url = @model.current_action.redirection_url || @action.redirection_url || request.referrer || "/cm_admin/#{@model.ar_model.table_name}/#{@response_object.id}"
format.html { redirect_to redirect_url, notice: "#{@action.formatted_name} was successful" }
else
error_messages = response_object.errors.full_messages.map { |error_message| "#{error_message}" }.join
format.html { redirect_to request.referrer, alert: "#{@action.formatted_name} was unsuccessful
" }
end
rescue StandardError => e
format.html { redirect_to request.referrer, alert: "#{@action.formatted_name} was unsuccessful
" }
end
end
end
end
end
def cm_custom_action_modal(params)
scoped_model = "CmAdmin::#{@model.name}Policy::#{params[:action_name].classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve
@ar_object = fetch_ar_object(scoped_model, params[:id])
if params[:action_name] == 'destroy'
render partial: '/layouts/destroy_action_modal', locals: { ar_object: @ar_object }
else
custom_action = @model.available_actions.select { |x| x.name == params[:action_name].to_s }.first
render partial: '/layouts/custom_action_modal', locals: { custom_action:, ar_object: @ar_object }
end
end
def reset_sort_columns
@model = Model.find_by({ name: controller_name.classify })
@model.default_sort_column = nil
@model.default_sort_direction = nil
end
def fetch_drawer
@model = Model.find_by({ name: controller_name.classify })
return if @model.blank?
action_page_title = CmAdmin::Models::Action.find_by(@model, name: 'new').page_title
drawer_title = action_page_title.presence || "New #{@model&.formatted_name}"
@ar_object = @model.ar_model.new
render partial: 'layouts/drawer', locals: { drawer_title:, from_field_id: params[:from_field_id] }
end
def get_nested_table_fields(fields)
nested_table_fields = []
fields.each do |field|
if field.class == CmAdmin::Models::Row
nested_table_fields += field.sections.map(&:nested_table_fields).flatten
elsif field.class == CmAdmin::Models::Section
nested_table_fields += field.nested_table_fields.flatten
end
end
nested_table_fields.flatten
end
def resource_identifier
@ar_object, @associated_model, @associated_ar_object = custom_controller_action(action_name, params.permit!) if !@ar_object.present? && params[:id].present?
authorize @ar_object, policy_class: "CmAdmin::#{controller_name.classify}Policy".constantize if defined? "CmAdmin::#{controller_name.classify}Policy".constantize
aar_model = request.url.split('/')[-2].classify.constantize if params[:aar_id]
@associated_ar_object = fetch_ar_object(aar_model, params[:aar_id]) if params[:aar_id]
nested_fields = get_nested_table_fields(@model.available_fields[:new])
nested_fields += get_nested_table_fields(@model.available_fields[:edit])
@reflections = @model.ar_model.reflect_on_all_associations
nested_fields.each do |nested_field|
table_name = nested_field.field_name
reflection = @reflections.select { |x| x if x.name == table_name }.first
if reflection.macro == :has_many
@ar_object.send(table_name).build if action_name == 'new' || action_name == 'edit'
elsif action_name == 'new'
@ar_object.send(('build_' + table_name.to_s).to_sym)
end
end
end
def resource_responder
respond_to do |format|
if @ar_object.save
redirect_url = if params['referrer']
params['referrer']
elsif @current_action.redirect_to.present?
@current_action.redirect_to.call(@ar_object)
else
cm_admin.send("#{@model.name.underscore}_show_path", @ar_object)
end
ActiveStorage::Attachment.where(id: params['attachment_destroy_ids']).destroy_all if params['attachment_destroy_ids'].present?
notice = if @current_action&.name == 'new'
"#{@model&.formatted_name} was created"
elsif @current_action&.name == 'edit'
"#{@model&.formatted_name} was updated"
else
"#{@action&.formatted_name} #{@model&.formatted_name} was successful"
end
format.html { redirect_to redirect_url, allow_other_host: true, notice: }
name = if @ar_object.respond_to?(:formatted_name)
@ar_object.formatted_name
elsif @ar_object.respond_to?(:name)
@ar_object.name
else
@ar_object&.id
end
format.json { render json: { message: notice, data: { id: @ar_object&.id, name: } }, status: :ok }
else
format.html { render '/cm_admin/main/new', notice: "#{@action&.formatted_name} #{@model&.formatted_name} was unsuccessful" }
format.json do
formatted_error_response = @ar_object.errors.full_messages.map { |error_message| "#{error_message}" }.join
render json: { message: formatted_error_response }, status: :unprocessable_entity
end
end
end
end
def custom_controller_action(action_name, params)
@current_action = CmAdmin::Models::Action.find_by(@model, name: action_name.to_s)
return unless @current_action
scoped_model = "CmAdmin::#{@model.name}Policy::#{action_name.classify}Scope".constantize.new(Current.user, @model.ar_model.name.classify.constantize).resolve
@ar_object = fetch_ar_object(scoped_model, params[:id])
return @ar_object unless @current_action.child_records
child_records = @ar_object.send(@current_action.child_records)
child_records = apply_scopes(child_records)
@reflection = @model.ar_model.reflect_on_association(@current_action.child_records)
@associated_model = if @reflection.klass.column_names.include?('type')
CmAdmin::Model.find_by(name: @reflection.plural_name.classify)
else
CmAdmin::Model.find_by(name: @reflection.klass.name)
end
@associated_ar_object = if child_records.is_a? ActiveRecord::Relation
filter_by(params, child_records, parent_record: @ar_object, filter_params: @associated_model.filter_params(params))
else
child_records
end
[@ar_object, @associated_model, @associated_ar_object]
end
def apply_scopes(records)
@current_action.scopes.each do |scope|
records = records.send(scope)
end
records
end
def filter_by(params, records, parent_record: nil, filter_params: {}, sort_params: {})
filtered_result = OpenStruct.new
cm_model = @associated_model || @model
db_columns = cm_model.ar_model&.columns&.map { |x| x.name.to_sym }
sort_column = if db_columns.include?(params[:sort_column]&.to_sym)
params[:sort_column]
else
cm_model.default_sort_column
end
sort_direction = params[:sort_direction] || cm_model.default_sort_direction
records = "CmAdmin::#{@model.name}Policy::#{@current_action.name.classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve if records.nil?
records = records.order("#{sort_column} #{sort_direction}") if sort_column.present?
final_data = CmAdmin::Models::Filter.filtered_data(filter_params, records, cm_model.filters)
pagy, records = pagy(final_data)
filtered_result.data = records
filtered_result.pagy = pagy
filtered_result.parent_record = parent_record
filtered_result.associated_model = @associated_model.name if @associated_model
filtered_result
end
def kanban_filter_by(params, records, filter_params = {}, _sort_params = {})
filtered_result = OpenStruct.new
cm_model = @associated_model || @model
db_columns = cm_model.ar_model&.columns&.map { |x| x.name.to_sym }
if db_columns.include?(@current_action.sort_column)
@current_action.sort_column
else
'created_at'
end
records = "CmAdmin::#{@model.name}Policy::Scope".constantize.new(Current.user, @model.name.constantize).resolve if records.nil?
# records = records.order("#{sort_column} #{@current_action.sort_direction}")
final_data = CmAdmin::Models::Filter.filtered_data(filter_params, records, cm_model.filters)
filtered_result.data = {}
filtered_result.paging = {}
filtered_result.paging['next_page'] = true
group_record_count = final_data.group(params[:kanban_column_name] || @current_action.kanban_attr[:column_name]).count
per_page = params[:per_page] || 20
page = params[:page] || 1
max_page = (group_record_count.values.max.to_i / per_page.to_f).ceil
filtered_result.paging['next_page'] = (page.to_i < max_page)
filtered_result.column_count = group_record_count.reject { |key, _value| key.blank? }
column_names = @model.ar_model.send(params[:kanban_column_name]&.pluralize || @current_action.kanban_attr[:column_name].pluralize).keys
if @current_action.kanban_attr[:only].present?
column_names &= @current_action.kanban_attr[:only]
elsif @current_action.kanban_attr[:exclude].present?
column_names -= @current_action.kanban_attr[:exclude]
end
column_names.each do |column|
total_count = group_record_count[column]
filtered_result.data[column] = ''
next if page.to_i > (total_count.to_i / per_page.to_f).ceil
_, records = pagy(final_data.send(column), items: per_page.to_i)
filtered_result.data[column] = render_to_string partial: 'cm_admin/main/kanban_card', locals: { ar_collection: records }
end
filtered_result
end
def generate_nested_params(nested_table_field)
if nested_table_field.parent_field
ar_model = nested_table_field.parent_field.to_s.classify.constantize
table_name = ar_model.reflections.with_indifferent_access[nested_table_field.field_name.to_s].klass.table_name
else
table_name = @model.ar_model.reflections.with_indifferent_access[nested_table_field.field_name.to_s].klass.table_name
end
column_names = table_name.to_s.classify.constantize.column_names
column_names = column_names.map { |column_name| column_name.gsub('_cents', '') }
column_names = column_names.reject { |column_name| CmAdmin::REJECTABLE_FIELDS.include?(column_name) }.map(&:to_sym) + %i[id _destroy]
if nested_table_field.associated_fields
nested_table_field.associated_fields.each do |associated_field|
column_names << generate_nested_params(associated_field)
end
end
column_names += attachment_fields(table_name.to_s.classify.constantize)
Hash[
"#{table_name}_attributes",
column_names
]
end
def resource_params(params)
columns = @model.ar_model.columns_hash.map do |_key, ar_adapter|
ar_adapter.sql_type_metadata.sql_type.ends_with?('[]') ? Hash[ar_adapter.name, []] : ar_adapter.name.to_sym
end
columns += @model.ar_model.stored_attributes.values.flatten
permittable_fields = @model.additional_permitted_fields + columns.reject { |i| CmAdmin::REJECTABLE_FIELDS.include?(i) }
permittable_fields += attachment_fields(@model.ar_model.name.constantize)
nested_table_fields = get_nested_table_fields(@model.available_fields[:new])
nested_table_fields += get_nested_table_fields(@model.available_fields[:edit])
nested_fields = nested_table_fields.uniq.map do |nested_table_field|
generate_nested_params(nested_table_field)
end
permittable_fields += nested_fields
@model.ar_model.columns.map { |col| permittable_fields << col.name.split('_cents') if col.name.include?('_cents') }
params.require(@model.name.underscore.to_sym).permit(*permittable_fields)
end
def fetch_ar_object(model_object, id)
return model_object.friendly.find(id) if model_object.respond_to?(:friendly)
model_object.find(id)
end
private
def attachment_fields(model_object)
model_object.reflect_on_all_associations.map do |reflection|
next if reflection.options[:polymorphic]
if reflection.class.name.include?('HasOne')
reflection.name.to_s.gsub('_attachment', '').gsub('rich_text_', '').to_sym
elsif reflection.class.name.include?('HasMany')
Hash[reflection.name.to_s.gsub('_attachments', ''), []]
end
end.compact
end
end
end