1
0
Fork 0
mirror of https://github.com/paper-trail-gem/paper_trail.git synced 2022-11-09 11:33:19 -05:00
paper-trail-gem--paper_trail/lib/paper_trail/model_config.rb
2016-06-27 01:50:41 -04:00

197 lines
7.3 KiB
Ruby

require "active_support/core_ext"
module PaperTrail
# Configures an ActiveRecord model, mostly at application boot time, but also
# sometimes mid-request, with methods like enable/disable.
class ModelConfig
E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
belongs_to_required_by_default and has no effect. Please use :before
or disable belongs_to_required_by_default.
STR
def initialize(model_class)
@model_class = model_class
end
# Switches PaperTrail off for this class.
def disable
::PaperTrail.enabled_for_model(@model_class, false)
end
# Switches PaperTrail on for this class.
def enable
::PaperTrail.enabled_for_model(@model_class, true)
end
def enabled?
return false unless @model_class.include?(::PaperTrail::Model::InstanceMethods)
::PaperTrail.enabled_for_model?(@model_class)
end
# Adds a callback that records a version after a "create" event.
def on_create
@model_class.after_create :record_create, if: ->(m) { m.paper_trail.save_version? }
return if @model_class.paper_trail_options[:on].include?(:create)
@model_class.paper_trail_options[:on] << :create
end
# Adds a callback that records a version before or after a "destroy" event.
def on_destroy(recording_order = "before")
unless %w(after before).include?(recording_order.to_s)
raise ArgumentError, 'recording order can only be "after" or "before"'
end
if recording_order.to_s == "after" && cannot_record_after_destroy?
::ActiveSupport::Deprecation.warn(E_CANNOT_RECORD_AFTER_DESTROY)
end
@model_class.send "#{recording_order}_destroy", :record_destroy,
if: ->(m) { m.paper_trail.save_version? }
return if @model_class.paper_trail_options[:on].include?(:destroy)
@model_class.paper_trail_options[:on] << :destroy
end
# Adds a callback that records a version after an "update" event.
def on_update
@model_class.before_save :reset_timestamp_attrs_for_update_if_needed!, on: :update
@model_class.after_update :record_update, if: ->(m) { m.paper_trail.save_version? }
@model_class.after_update :clear_version_instance!
return if @model_class.paper_trail_options[:on].include?(:update)
@model_class.paper_trail_options[:on] << :update
end
# Set up `@model_class` for PaperTrail. Installs callbacks, associations,
# "class attributes", instance methods, and more.
# @api private
def setup(options = {})
options[:on] ||= [:create, :update, :destroy]
options[:on] = Array(options[:on]) # Support single symbol
@model_class.send :include, ::PaperTrail::Model::InstanceMethods
if ::ActiveRecord::VERSION::STRING < "4.2"
@model_class.send :extend, AttributeSerializers::LegacyActiveRecordShim
end
setup_options(options)
setup_associations(options)
setup_transaction_callbacks
setup_callbacks_from_options options[:on]
setup_callbacks_for_habtm options[:join_tables]
end
def version_class
@_version_class ||= @model_class.version_class_name.constantize
end
private
def active_record_gem_version
Gem::Version.new(ActiveRecord::VERSION::STRING)
end
def cannot_record_after_destroy?
Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
::ActiveRecord::Base.belongs_to_required_by_default
end
def setup_associations(options)
@model_class.class_attribute :version_association_name
@model_class.version_association_name = options[:version] || :version
# The version this instance was reified from.
@model_class.send :attr_accessor, @model_class.version_association_name
@model_class.class_attribute :version_class_name
@model_class.version_class_name = options[:class_name] || "PaperTrail::Version"
@model_class.class_attribute :versions_association_name
@model_class.versions_association_name = options[:versions] || :versions
@model_class.send :attr_accessor, :paper_trail_event
# In rails 4, the `has_many` syntax for specifying order uses a lambda.
if ::ActiveRecord::VERSION::MAJOR >= 4
@model_class.has_many(
@model_class.versions_association_name,
-> { order(model.timestamp_sort_order) },
class_name: @model_class.version_class_name,
as: :item
)
else
@model_class.has_many(
@model_class.versions_association_name,
class_name: @model_class.version_class_name,
as: :item,
order: @model_class.paper_trail_version_class.timestamp_sort_order
)
end
end
# Adds callbacks to record changes to habtm associations such that on save
# the previous version of the association (if changed) can be interpreted.
def setup_callbacks_for_habtm(join_tables)
@model_class.send :attr_accessor, :paper_trail_habtm
@model_class.class_attribute :paper_trail_save_join_tables
@model_class.paper_trail_save_join_tables = Array.wrap(join_tables)
@model_class.reflect_on_all_associations(:has_and_belongs_to_many).
reject { |a| @model_class.paper_trail_options[:skip].include?(a.name.to_s) }.
each { |a|
added_callback = lambda do |*args|
update_habtm_state(a.name, :before_add, args[-2], args.last)
end
removed_callback = lambda do |*args|
update_habtm_state(a.name, :before_remove, args[-2], args.last)
end
@model_class.send(:"before_add_for_#{a.name}").send(:<<, added_callback)
@model_class.send(:"before_remove_for_#{a.name}").send(:<<, removed_callback)
}
end
def setup_callbacks_from_options(options_on = [])
options_on.each do |event|
public_send("on_#{event}")
end
end
def setup_options(options)
@model_class.class_attribute :paper_trail_options
@model_class.paper_trail_options = options.dup
[:ignore, :skip, :only].each do |k|
@model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
flatten.
compact.
map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
end
@model_class.paper_trail_options[:meta] ||= {}
if @model_class.paper_trail_options[:save_changes].nil?
@model_class.paper_trail_options[:save_changes] = true
end
end
# Reset the transaction id when the transaction is closed.
def setup_transaction_callbacks
@model_class.after_commit { PaperTrail.clear_transaction_id }
@model_class.after_rollback { PaperTrail.clear_transaction_id }
@model_class.after_rollback { paper_trail.clear_rolled_back_versions }
end
def update_habtm_state(name, callback, model, assoc)
model.paper_trail_habtm ||= {}
model.paper_trail_habtm[name] ||= { removed: [], added: [] }
state = model.paper_trail_habtm[name]
assoc_id = assoc.id
case callback
when :before_add
state[:added] |= [assoc_id]
state[:removed] -= [assoc_id]
when :before_remove
state[:removed] |= [assoc_id]
state[:added] -= [assoc_id]
else
raise "Invalid callback: #{callback}"
end
end
end
end