module PgEngine
module Resource
# rubocop:disable Metrics/PerceivedComplexity
def self.included(clazz)
class << clazz
# This is a per class variable, all subclasses of clazz inherit it
# BUT **the values are independent between all of them**
attr_accessor :nested_class, :nested_key, :clase_modelo
# TODO: separar los endpoints de la lógica de filtros para evitar esta variable
# en Agenda
attr_accessor :skip_default_breadcrumb
# Skip set_instancia_modelo && authorize
attr_accessor :skip_default_hooks
end
clazz.delegate :nested_key, :nested_class, :clase_modelo, to: clazz
clazz.helper_method :nested_class, :nested_key, :clase_modelo
clazz.helper_method :nested_record, :nested_id
clazz.helper_method :atributos_para_listar
clazz.helper_method :atributos_para_mostrar
clazz.helper_method :current_page_size
clazz.helper_method :show_filters?
clazz.helper_method :available_page_sizes
clazz.helper_method :column_options_for
clazz.helper_method :instancia_modelo
clazz.before_action do
# TODO: quitar esto, que se use el attr_accessor
# o sea, quitar todas las referencias a @clase_modelo
@clase_modelo = clase_modelo
end
clazz.before_action(only: %i[index archived], unless: -> { clazz.skip_default_hooks }) do
authorize clase_modelo
end
clazz.before_action :set_instancia_modelo,
only: %i[new create show edit update destroy archive restore],
unless: -> { clazz.skip_default_hooks }
clazz.before_action unless: -> { clazz.skip_default_breadcrumb } do
if nested_record.present?
# Link al nested, siempre que sea no sea un embedded frame
# ya que en tal caso se supone que el nested está visible
# en el main frame. Además, si es un modal abierto desde
# el nested record, no muestro el link al mismo.
unless frame_embedded?
if modal_targeted? && referred_by?(nested_record)
add_breadcrumb nested_record.decorate.to_s_short
else
add_breadcrumb nested_record.decorate.to_s_short,
path_for(nested_record.decorate.target_object)
end
end
if action_name == 'archived'
# En el listado de archivados tiene que haber un link al listado
# principal
add_breadcrumb clase_modelo.nombre_plural,
path_for([pg_namespace, nested_record, clase_modelo])
else
# Texto de index pero sin link, porque se supone que es un
# embedded index o es un show dentro de un embedded y por ende
# el paso atrás no tiene que ser el listado sino el show del parent
add_breadcrumb clase_modelo.nombre_plural
end
elsif !modal_targeted?
# Link al index, siempre que no sea un modal, porque en tal
# caso se supone que el index está visible en el main frame
if clase_modelo.present?
add_breadcrumb clase_modelo.nombre_plural,
path_for([pg_namespace, nested_record, clase_modelo])
else
# :nocov:
pg_warn 'clase_modelo is nil'
# :nocov:
end
end
end
clazz.layout :set_layout
end
# rubocop:enable Metrics/PerceivedComplexity
def column_options_for(_object, _attribute)
{ class: 'text-nowrap' }
end
def referred_by?(object)
url_for(object.decorate.target_object) == request.referer
end
def nested_id
return unless nested_key.present? && nested_class.present?
id = params[nested_key]
# if using hashid-rails
if nested_class.respond_to? :decode_id
id = nested_class.decode_id(id)
end
id
end
def nested_record
return if nested_id.blank?
nested_class.find(nested_id)
rescue ActiveRecord::RecordNotFound => e
pg_warn(e)
raise PgEngine::PageNotFoundError
end
def accepts_turbo_stream?
request.headers['Accept'].present? &&
request.headers['Accept'].include?('text/vnd.turbo-stream.html')
end
def respond_with_modal?
can_open_modal? || modal_targeted?
end
def can_open_modal?
request.get? &&
clase_modelo.default_modal &&
accepts_turbo_stream? &&
!in_modal?
end
def set_layout
if action_name.in? %w[index archived]
'pg_layout/base'
elsif action_name == 'show'
'pg_layout/show'
else
'pg_layout/containerized'
end
end
# Public endpoints
def abrir_modal
pg_respond_abrir_modal
end
def buscar
pg_respond_buscar
end
def archived
add_breadcrumb 'Archivados'
@index_url = index_url
@collection = filtros_y_policy(atributos_para_buscar, 'discarded_at desc', archived: true)
pg_respond_index(archived: true)
end
def index
@collection = filtros_y_policy(atributos_para_buscar, default_sort)
pg_respond_index(archived: false)
end
def show
pg_respond_show
end
# TODO!: refactor
def respond_with_modal(component)
content = component.render_in(view_context)
if can_open_modal?
modal = ModalComponent.new.with_content(content)
render turbo_stream: turbo_stream.append_all('body', modal)
else
render html: content
end
end
def new
if can_open_modal?
path = [request.path, request.query_string].compact.join('?')
component = ModalContentComponent.new(src: path)
respond_with_modal(component)
else
add_breadcrumb instancia_modelo.submit_default_value
end
end
def edit
if can_open_modal?
path = [request.path, request.query_string].compact.join('?')
component = ModalContentComponent.new(src: path)
respond_with_modal(component)
else
if (modal_targeted? && referred_by?(instancia_modelo)) ||
!policy(instancia_modelo.object).show?
add_breadcrumb instancia_modelo.to_s_short
else
add_breadcrumb instancia_modelo.to_s_short,
instancia_modelo.target_object
end
add_breadcrumb 'Modificando'
end
end
def create
pg_respond_create
end
def update
pg_respond_update
end
def destroy
pg_respond_destroy(instancia_modelo, params[:land_on])
end
def archive
discard_undiscard(:discard)
end
def restore
discard_undiscard(:undiscard)
end
# End public endpoints
protected
def discard_undiscard(method)
if instancia_modelo.send(method)
if accepts_turbo_stream?
body = <<~HTML.html_safe
HTML
render turbo_stream: turbo_stream.append(current_turbo_frame, body)
else
redirect_to instancia_modelo.decorate.target_object
end
else
message = I18n.t('pg_engine.resource_not_updated', model: instancia_modelo.class)
if accepts_turbo_stream?
flash.now[:alert] = message
render turbo_stream: render_turbo_stream_flash_messages, status: :unprocessable_entity
else
flash[:alert] = message
redirect_to instancia_modelo.decorate.target_object
end
end
end
def index_url
path_for([pg_namespace, nested_record, clase_modelo])
end
def default_sort
'id desc'
end
def available_page_sizes
[5, 10, 20, 30, 50, 100].push(current_page_size).uniq.sort
end
def show_filters_by_default?
true
end
def filters_applied?
params[RansackMemory::Core.config[:param].presence || :q].present?
end
def session_key_identifier
::RansackMemory::Core.config[:session_key_format]
.gsub('%controller_name%', controller_path.parameterize.underscore)
.gsub('%action_name%', action_name)
.gsub('%request_format%', request.format.symbol.to_s)
.gsub('%turbo_frame%', request.headers['Turbo-Frame'] || 'top')
# TODO!!: rename to main?
end
def show_filters?
return true if filters_applied?
idtf = "show-filters_#{session_key_identifier}"
if params[:ocultar_filtros]
session[idtf] = false
elsif params[:mostrar_filtros]
session[idtf] = true
end
if session[idtf].nil?
show_filters_by_default?
else
session[idtf]
end
end
def current_page_size
aux = params[:page_size].presence&.to_i
if aux.present? && aux.positive?
session[page_size_session_key] = aux
end
session[page_size_session_key].presence || default_page_size
end
def page_size_session_key
"page_size_#{session_key_identifier}"
end
def default_page_size
10
end
def pg_respond_update
object = instancia_modelo
if (@saved = object.save)
respond_to do |format|
format.html do
if params[:inline_attribute].present?
render InlineShowComponent.new(object, params[:inline_attribute], record_updated: true),
layout: false
elsif in_modal?
body = <<~HTML.html_safe
HTML
render html: ModalContentComponent.new.with_content(body)
.render_in(view_context)
else
redirect_to object.decorate.target_object
end
end
format.json do
render json: object.decorate.as_json
end
end
elsif params[:inline_attribute].present?
render InlineEditComponent.new(object, params[:inline_attribute]),
layout: false, status: :unprocessable_entity
else
add_breadcrumb instancia_modelo.decorate.to_s_short, instancia_modelo.decorate.target_object
add_breadcrumb 'Modificando'
# TODO: esto solucionaría el problema?
# self.instancia_modelo = instancia_modelo.decorate
#
render :edit, status: :unprocessable_entity
end
end
def pg_respond_create
object = instancia_modelo
if (@saved = object.save)
if in_modal?
body = <<~HTML.html_safe
HTML
render turbo_stream: turbo_stream.append(current_turbo_frame, body)
else
redirect_to object.decorate.target_object
end
else
add_breadcrumb instancia_modelo.decorate.submit_default_value
# TODO: esto solucionaría el problema?
# self.instancia_modelo = instancia_modelo.decorate
render :new, status: :unprocessable_entity
end
end
def pg_respond_index(archived:)
respond_to do |format|
format.json { render json: @collection }
format.html { render_listing(archived:) }
format.xlsx do
render xlsx: 'download',
filename: "#{clase_modelo.nombre_plural.gsub(' ', '-').downcase}" \
"#{action_name == 'archived' ? '-archivados' : ''}" \
"-#{Time.zone.now.strftime('%Y-%m-%d-%H.%M.%S')}.xlsx"
end
end
end
def pg_respond_show
if can_open_modal?
path = [request.path, request.query_string].compact.join('?')
component = ModalContentComponent.new(src: path)
respond_with_modal(component)
else
add_breadcrumb instancia_modelo.to_s_short, instancia_modelo.target_object
end
end
def land_on_url(land_on)
case land_on
when 'index'
index_url
else
# :nocov:
pg_warn "Unrecognized land_on: #{land_on}"
instancia_modelo.decorate.target_object
# :nocov:
end
end
# rubocop:disable Metrics/PerceivedComplexity
def pg_respond_destroy(model, land_on = nil)
if destroy_model(model)
# TODO!!: rename to main
if turbo_frame? && current_turbo_frame != 'top'
body = <<~HTML.html_safe
HTML
render turbo_stream: turbo_stream.append(current_turbo_frame, body)
elsif land_on.present?
redirect_to land_on_url(land_on),
notice: I18n.t('pg_engine.resource_destroyed', model: model.class),
status: :see_other
elsif accepts_turbo_stream?
body = <<~HTML.html_safe
HTML
render turbo_stream: turbo_stream.append_all('body', body)
else
redirect_back(fallback_location: root_path,
notice: I18n.t('pg_engine.resource_destroyed', model: model.class),
status: 303)
end
elsif in_modal?
flash.now[:alert] = @error_message
render turbo_stream: render_turbo_stream_flash_messages(to: '.modal-body .flash')
elsif accepts_turbo_stream?
flash.now[:alert] = @error_message
render turbo_stream: render_turbo_stream_flash_messages
else
flash[:alert] = @error_message
redirect_back(fallback_location: root_path, status: 303)
end
end
# rubocop:enable Metrics/PerceivedComplexity
def destroy_model(model)
@error_message = I18n.t('pg_engine.resource_not_destroyed', model: model.class)
begin
return true if model.destroy
@error_message = model.errors.full_messages.join(', ').presence || @error_message
false
rescue ActiveRecord::InvalidForeignKey => e
pg_warn(e)
@error_message = I18n.t('pg_engine.resource_not_destroyed_because_associated', model: model.class)
end
false
end
def render_listing(archived:)
total = @collection.count
current_page = params[:page].presence&.to_i || 1
if current_page_size * (current_page - 1) >= total
current_page = (total.to_f / current_page_size).ceil
end
@collection = @collection.page(current_page).per(current_page_size)
@records_filtered = default_scope_for_current_model(archived:).any? if @filtros.present? && @collection.empty?
render :index
end
def buscar_instancia
if Object.const_defined?('FriendlyId') && clase_modelo.is_a?(FriendlyId)
clase_modelo.friendly.find(params[:id])
elsif clase_modelo.respond_to? :find_by_hashid!
clase_modelo.find_by_hashid!(params[:id])
else
clase_modelo.find(params[:id])
end
rescue ActiveRecord::RecordNotFound
raise PgEngine::PageNotFoundError
end
def set_instancia_modelo
if action_name.in? %w[new create]
self.instancia_modelo = clase_modelo.new(modelo_params)
authorize(instancia_modelo)
if nested_id.present?
instancia_modelo.send("#{nested_key}=", nested_id)
end
else
self.instancia_modelo = buscar_instancia
authorize(instancia_modelo)
instancia_modelo.assign_attributes(modelo_params) if action_name.in? %w[update]
end
# TODO: problema en create y update cuando falla la validacion
# Reproducir el error antes de arreglarlo
self.instancia_modelo = instancia_modelo.decorate if action_name.in? %w[show edit new]
end
def instancia_modelo=(val)
instance_variable_set(:"@#{nombre_modelo}", val)
end
def instancia_modelo
instance_variable_get(:"@#{nombre_modelo}")
end
def modelo_params
if action_name == 'new'
params.permit(atributos_permitidos)
else
params.require(nombre_modelo).permit(atributos_permitidos)
end
end
def nombre_modelo
clase_modelo.name.underscore
end
def filtros_y_policy(campos, dflt_sort = nil, archived: false)
if campos.any?
@filtros = PgEngine::FiltrosBuilder.new(
self, clase_modelo, campos
)
end
scope = default_scope_for_current_model(archived:)
shared_context = Ransack::Adapters::ActiveRecord::Context.new(scope)
@q = clase_modelo.ransack(params[:q], context: shared_context)
@q.sorts = dflt_sort if @q.sorts.empty? && dflt_sort.present?
shared_context.evaluate(@q)
end
def soft_delete_filter(scope, archived:)
return scope unless scope.respond_to?(:discarded)
if nested_id.present?
if archived
scope.discarded
else
scope.undiscarded
end
elsif archived
scope.unkept
else
scope.kept
end
end
def default_scope_for_current_model(archived: false)
scope = policy_scope(clase_modelo)
if nested_id.present?
scope = scope.where(nested_key => nested_id)
end
soft_delete_filter(scope, archived:)
end
end
end