# frozen_string_literal: true module PaperTrailScrapbook # Class Changes provides detailed attribute by attribute analysis # # @author Timothy Chambers # class Changes include Concord.new(:version) include Adamantium::Flat include PaperTrailScrapbook::VersionHelpers POLYMORPH_BT_INDICATOR = '*' delegate :object_changes, to: :version def initialize(*) super build_associations changes end BULLET = ' •' # Attribute change analysis # # # @return [String] Summary analysis of changes # def change_log text = changes .map { |k, v| digest(k, v) } .compact .join("\n") text = text.gsub(' id:', ':') if PaperTrailScrapbook.config.drop_id_suffix text end private def polymorphic?(x) x.to_s.start_with?(POLYMORPH_BT_INDICATOR) end def digest(key, values) old, new = values return if old.nil? && (new.nil? || new.eql?('')) || (old == new && !creating?) "#{BULLET} #{key.tr('_', ' ')}: #{detailed_analysis(key, new, old)}" end def detailed_analysis(key, new, old) if creating? find_value(key, new).to_s elsif old.nil? "#{find_value(key, new)} added" elsif new.nil? "#{find_value(key, old)} was *removed*" else "#{find_value(key, old)} -> #{find_value(key, new)}" end end def creating? version.event.eql?('create') end def find_value(key, value) return value.to_s unless build_associations.key?(key) return '*empty*' unless value begin assoc_target(key).find(value).to_s.to_s + "[#{value}]" rescue StandardError "*not found*[#{value}]" end end def assoc_target(key) x = build_associations[key] return x unless polymorphic?(x) ref = x[1..-1] + '_type' # try object changes to see if the belongs_to class is specified latest_class = changes[ref]&.last if latest_class.nil? && create? # try the db default class # for creates where the object changes do not specify this it # is most likely because the default == type selected so # the default was not changed and therefore is not in # object changes orig_instance = Object.const_get(version.item_type.classify).new latest_class = orig_instance[ref.to_sym] end Object.const_get(latest_class.classify) end def assoc_klass(name, options = {}) direct_class = options[:class_name] poly = options[:polymorphic] return direct_class if !poly && direct_class && !direct_class.is_a?(String) poly ? POLYMORPH_BT_INDICATOR + name.to_s : Object.const_get((direct_class || name.to_s).classify) rescue StandardError Object.const_set(name.to_s.classify, Class.new) end def klass assoc_klass(version.item_type) end def build_associations @build_associations ||= Hash[ klass .reflect_on_all_associations .select { |a| a.macro.equal?(:belongs_to) } .map { |x| [x.foreign_key.to_s, assoc_klass(x.name, x.options)] } ] end def changes @changes ||= if object_changes YAML .load(object_changes) .except(*PaperTrailScrapbook.config.scrub_columns) else {} end end end end