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
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.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 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,
nested_record.decorate.target_object
end
end
# Texto de index pero sin link, porque se supone que es un
# embedded index o viene de tal
add_breadcrumb clase_modelo.nombre_plural
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,
url_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 == 'index'
'pg_layout/base'
else
'pg_layout/containerized'
end
end
# Public endpoints
def abrir_modal
pg_respond_abrir_modal
end
def buscar
pg_respond_buscar
end
def index
@collection = filtros_y_policy(atributos_para_buscar, default_sort)
pg_respond_index
end
def show
pg_respond_show
end
# FIXME: 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)
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[:redirect_to])
end
# End public endpoints
protected
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')
# FIXME: 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 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
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
respond_to do |format|
format.json { render json: @collection }
format.html { render_listing }
format.xlsx do
render xlsx: 'download',
filename: "#{clase_modelo.nombre_plural.gsub(' ', '-').downcase}" \
"-#{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 destroyed_message(model)
"#{model.model_name.human} #{model.gender == 'f' ? 'borrada' : 'borrado'}"
end
# rubocop:disable Metrics/PerceivedComplexity
def pg_respond_destroy(model, redirect_url = nil)
if destroy_model(model)
# FIXME: 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 redirect_url.present?
redirect_to redirect_url, notice: destroyed_message(model), 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: destroyed_message(model), 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 = 'No se pudo eliminar el registro'
begin
destroy_method = model.respond_to?(:discard) ? :discard : :destroy
return true if model.send(destroy_method)
@error_message = model.errors.full_messages.join(', ').presence || @error_message
false
rescue ActiveRecord::InvalidForeignKey => e
model_name = t("activerecord.models.#{model.class.name.underscore}")
@error_message = "#{model_name} no se pudo borrar porque tiene elementos asociados."
pg_warn(e)
end
false
end
def render_listing
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.any? if @collection.empty?
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)
if nested_id.present?
instancia_modelo.send("#{nested_key}=", nested_id)
end
else
self.instancia_modelo = buscar_instancia
instancia_modelo.assign_attributes(modelo_params) if action_name.in? %w[update]
end
authorize(instancia_modelo)
# 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)
@filtros = PgEngine::FiltrosBuilder.new(
self, clase_modelo, campos
)
scope = policy_scope(clase_modelo)
if nested_id.present?
scope = scope.where(nested_key => nested_id)
scope = scope.undiscarded if scope.respond_to?(:undiscarded)
elsif scope.respond_to?(:kept)
scope = scope.kept
end
# Soft deleted
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 default_scope_for_current_model
scope = policy_scope(clase_modelo)
if nested_id.present?
scope = scope.where(nested_key => nested_id)
# Skip nested discarded check
scope = scope.undiscarded if scope.respond_to?(:undiscarded)
elsif scope.respond_to?(:kept)
scope = scope.kept
end
# Soft deleted, including nested discarded check
scope
end
end
end