2013-12-03 16:00:04 -05:00
|
|
|
require 'active_support/concern'
|
|
|
|
|
|
|
|
module PaperTrail
|
|
|
|
module VersionConcern
|
|
|
|
extend ::ActiveSupport::Concern
|
|
|
|
|
|
|
|
included do
|
|
|
|
belongs_to :item, :polymorphic => true
|
2015-01-20 15:25:06 -05:00
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Since the test suite has test coverage for this, we want to declare
|
|
|
|
# the association when the test suite is running. This makes it pass when
|
|
|
|
# DB is not initialized prior to test runs such as when we run on Travis
|
|
|
|
# CI (there won't be a db in `test/dummy/db/`).
|
2015-03-25 19:17:22 -04:00
|
|
|
if PaperTrail.config.track_associations?
|
2015-01-20 15:25:06 -05:00
|
|
|
has_many :version_associations, :dependent => :destroy
|
|
|
|
end
|
2014-02-25 09:02:36 -05:00
|
|
|
|
2013-12-03 16:00:04 -05:00
|
|
|
validates_presence_of :event
|
2014-10-20 13:04:51 -04:00
|
|
|
|
2015-01-20 16:07:33 -05:00
|
|
|
if PaperTrail.active_record_protected_attributes?
|
2015-11-01 23:56:53 -05:00
|
|
|
attr_accessible(
|
|
|
|
:item_type,
|
|
|
|
:item_id,
|
|
|
|
:event,
|
|
|
|
:whodunnit,
|
|
|
|
:object,
|
|
|
|
:object_changes,
|
|
|
|
:transaction_id,
|
|
|
|
:created_at
|
|
|
|
)
|
2015-01-20 16:07:33 -05:00
|
|
|
end
|
2013-12-03 16:00:04 -05:00
|
|
|
|
|
|
|
after_create :enforce_version_limit!
|
2014-02-25 09:02:36 -05:00
|
|
|
|
|
|
|
scope :within_transaction, lambda { |id| where :transaction_id => id }
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
module ClassMethods
|
|
|
|
def with_item_keys(item_type, item_id)
|
|
|
|
where :item_type => item_type, :item_id => item_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def creates
|
|
|
|
where :event => 'create'
|
|
|
|
end
|
|
|
|
|
|
|
|
def updates
|
|
|
|
where :event => 'update'
|
|
|
|
end
|
|
|
|
|
|
|
|
def destroys
|
|
|
|
where :event => 'destroy'
|
|
|
|
end
|
|
|
|
|
|
|
|
def not_creates
|
|
|
|
where 'event <> ?', 'create'
|
|
|
|
end
|
|
|
|
|
2015-11-05 16:19:50 -05:00
|
|
|
# Returns versions after `obj`.
|
|
|
|
#
|
|
|
|
# @param obj - a `Version` or a timestamp
|
|
|
|
# @param timestamp_arg - boolean - When true, `obj` is a timestamp.
|
|
|
|
# Default: false.
|
|
|
|
# @return `ActiveRecord::Relation`
|
|
|
|
# @api public
|
2014-04-01 13:38:53 -04:00
|
|
|
def subsequent(obj, timestamp_arg = false)
|
|
|
|
if timestamp_arg != true && self.primary_key_is_int?
|
2014-05-28 19:18:53 -04:00
|
|
|
return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
|
2014-04-01 13:38:53 -04:00
|
|
|
end
|
|
|
|
|
2013-12-03 16:00:04 -05:00
|
|
|
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
2014-05-28 19:18:53 -04:00
|
|
|
where(arel_table[PaperTrail.timestamp_field].gt(obj)).order(self.timestamp_sort_order)
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
2015-11-05 16:19:50 -05:00
|
|
|
# Returns versions before `obj`.
|
|
|
|
#
|
|
|
|
# @param obj - a `Version` or a timestamp
|
|
|
|
# @param timestamp_arg - boolean - When true, `obj` is a timestamp.
|
|
|
|
# Default: false.
|
|
|
|
# @return `ActiveRecord::Relation`
|
|
|
|
# @api public
|
2014-04-01 13:38:53 -04:00
|
|
|
def preceding(obj, timestamp_arg = false)
|
|
|
|
if timestamp_arg != true && self.primary_key_is_int?
|
2014-05-28 19:18:53 -04:00
|
|
|
return where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
|
2014-04-01 13:38:53 -04:00
|
|
|
end
|
|
|
|
|
2013-12-03 16:00:04 -05:00
|
|
|
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
2014-05-28 19:18:53 -04:00
|
|
|
where(arel_table[PaperTrail.timestamp_field].lt(obj)).order(self.timestamp_sort_order('desc'))
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def between(start_time, end_time)
|
2014-05-28 19:18:53 -04:00
|
|
|
where(
|
|
|
|
arel_table[PaperTrail.timestamp_field].gt(start_time).
|
|
|
|
and(arel_table[PaperTrail.timestamp_field].lt(end_time))
|
|
|
|
).order(self.timestamp_sort_order)
|
2014-04-01 13:38:53 -04:00
|
|
|
end
|
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Defaults to using the primary key as the secondary sort order if
|
|
|
|
# possible.
|
2014-05-28 19:18:53 -04:00
|
|
|
def timestamp_sort_order(direction = 'asc')
|
|
|
|
[arel_table[PaperTrail.timestamp_field].send(direction.downcase)].tap do |array|
|
|
|
|
array << arel_table[primary_key].send(direction.downcase) if self.primary_key_is_int?
|
2014-05-09 11:01:33 -04:00
|
|
|
end
|
2014-04-01 13:38:53 -04:00
|
|
|
end
|
|
|
|
|
2014-06-07 10:42:17 -04:00
|
|
|
# Performs an attribute search on the serialized object by invoking the
|
|
|
|
# identically-named method in the serializer being used.
|
2014-06-26 16:11:22 -04:00
|
|
|
def where_object(args = {})
|
|
|
|
raise ArgumentError, 'expected to receive a Hash' unless args.is_a?(Hash)
|
2014-06-07 10:42:17 -04:00
|
|
|
|
2015-04-09 13:49:12 -04:00
|
|
|
if columns_hash['object'].type == :jsonb
|
|
|
|
where_conditions = "object @> '#{args.to_json}'::jsonb"
|
2015-04-09 14:24:09 -04:00
|
|
|
elsif columns_hash['object'].type == :json
|
|
|
|
where_conditions = args.map do |field, value|
|
|
|
|
"object->>'#{field}' = '#{value}'"
|
|
|
|
end
|
|
|
|
where_conditions = where_conditions.join(" AND ")
|
2015-04-06 13:06:14 -04:00
|
|
|
else
|
|
|
|
arel_field = arel_table[:object]
|
|
|
|
|
|
|
|
where_conditions = args.map do |field, value|
|
|
|
|
PaperTrail.serializer.where_object_condition(arel_field, field, value)
|
|
|
|
end.reduce do |condition1, condition2|
|
|
|
|
condition1.and(condition2)
|
|
|
|
end
|
2014-06-07 10:42:17 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
where(where_conditions)
|
|
|
|
end
|
|
|
|
|
2015-01-07 16:57:45 -05:00
|
|
|
def where_object_changes(args = {})
|
|
|
|
raise ArgumentError, 'expected to receive a Hash' unless args.is_a?(Hash)
|
|
|
|
|
2015-04-09 14:56:26 -04:00
|
|
|
if columns_hash['object_changes'].type == :jsonb
|
|
|
|
args.each { |field, value| args[field] = [value] }
|
2015-04-09 13:49:12 -04:00
|
|
|
where_conditions = "object_changes @> '#{args.to_json}'::jsonb"
|
2015-04-09 15:27:15 -04:00
|
|
|
elsif columns_hash['object'].type == :json
|
|
|
|
where_conditions = args.map do |field, value|
|
|
|
|
"((object_changes->>'#{field}' ILIKE '[#{value.to_json},%') OR (object_changes->>'#{field}' ILIKE '[%,#{value.to_json}]%'))"
|
|
|
|
end
|
|
|
|
where_conditions = where_conditions.join(" AND ")
|
2015-04-06 13:06:14 -04:00
|
|
|
else
|
|
|
|
arel_field = arel_table[:object_changes]
|
|
|
|
|
|
|
|
where_conditions = args.map do |field, value|
|
|
|
|
PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
|
|
|
|
end.reduce do |condition1, condition2|
|
|
|
|
condition1.and(condition2)
|
|
|
|
end
|
2015-01-07 16:57:45 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
where(where_conditions)
|
|
|
|
end
|
|
|
|
|
2014-04-01 13:38:53 -04:00
|
|
|
def primary_key_is_int?
|
|
|
|
@primary_key_is_int ||= columns_hash[primary_key].type == :integer
|
2014-05-09 11:43:30 -04:00
|
|
|
rescue
|
|
|
|
true
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Returns whether the `object` column is using the `json` type supported
|
|
|
|
# by PostgreSQL.
|
2013-12-03 16:00:04 -05:00
|
|
|
def object_col_is_json?
|
2015-04-09 13:26:24 -04:00
|
|
|
[:json, :jsonb].include?(columns_hash['object'].type)
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Returns whether the `object_changes` column is using the `json` type
|
|
|
|
# supported by PostgreSQL.
|
2013-12-03 16:00:04 -05:00
|
|
|
def object_changes_col_is_json?
|
2015-04-09 13:26:24 -04:00
|
|
|
[:json, :jsonb].include?(columns_hash['object_changes'].try(:type))
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Restore the item from this version.
|
|
|
|
#
|
2015-08-03 16:45:42 -04:00
|
|
|
# Optionally this can also restore all :has_one and :has_many (including
|
|
|
|
# has_many :through) associations as they were "at the time", if they are
|
|
|
|
# also being versioned by PaperTrail.
|
2013-12-03 16:00:04 -05:00
|
|
|
#
|
|
|
|
# Options:
|
2015-08-03 16:45:42 -04:00
|
|
|
#
|
|
|
|
# - :has_one
|
|
|
|
# - `true` - Also reify has_one associations.
|
|
|
|
# - `false - Default.
|
|
|
|
# - :has_many
|
|
|
|
# - `true` - Also reify has_many and has_many :through associations.
|
|
|
|
# - `false` - Default.
|
|
|
|
# - :mark_for_destruction
|
|
|
|
# - `true` - Mark the has_one/has_many associations that did not exist in
|
|
|
|
# the reified version for destruction, instead of removing them.
|
|
|
|
# - `false` - Default. Useful for persisting the reified version.
|
|
|
|
# - :dup
|
|
|
|
# - `false` - Default.
|
|
|
|
# - `true` - Always create a new object instance. Useful for
|
|
|
|
# comparing two versions of the same object.
|
|
|
|
# - :unversioned_attributes
|
|
|
|
# - `:nil` - Default. Attributes undefined in version record are set to
|
|
|
|
# nil in reified record.
|
|
|
|
# - `:preserve` - Attributes undefined in version record are not modified.
|
|
|
|
#
|
2013-12-03 16:00:04 -05:00
|
|
|
def reify(options = {})
|
|
|
|
return nil if object.nil?
|
|
|
|
without_identity_map do
|
2015-08-03 18:37:40 -04:00
|
|
|
::PaperTrail::Reifier.reify(self, options)
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Returns what changed in this version of the item.
|
|
|
|
# `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does
|
|
|
|
# not have an `object_changes` text column.
|
2013-12-03 16:00:04 -05:00
|
|
|
def changeset
|
|
|
|
return nil unless self.class.column_names.include? 'object_changes'
|
2015-11-27 22:29:57 -05:00
|
|
|
_changes = HashWithIndifferentAccess.new(object_changes_deserialized)
|
|
|
|
@changeset ||= _changes.tap do |changes|
|
2014-12-29 14:38:32 -05:00
|
|
|
if PaperTrail.serialized_attributes?
|
2015-05-08 10:42:22 -04:00
|
|
|
item_type.constantize.unserialize_attribute_changes_for_paper_trail!(changes)
|
2014-12-29 14:38:32 -05:00
|
|
|
end
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
rescue
|
|
|
|
{}
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns who put the item into the state stored in this version.
|
2015-05-08 13:28:47 -04:00
|
|
|
def paper_trail_originator
|
|
|
|
@paper_trail_originator ||= previous.whodunnit rescue nil
|
|
|
|
end
|
|
|
|
|
2013-12-03 16:00:04 -05:00
|
|
|
def originator
|
2015-07-13 13:36:34 -04:00
|
|
|
::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
|
2015-05-08 13:28:47 -04:00
|
|
|
self.paper_trail_originator
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Returns who changed the item from the state it had in this version. This
|
|
|
|
# is an alias for `whodunnit`.
|
2013-12-03 16:00:04 -05:00
|
|
|
def terminator
|
|
|
|
@terminator ||= whodunnit
|
|
|
|
end
|
|
|
|
alias_method :version_author, :terminator
|
|
|
|
|
|
|
|
def sibling_versions(reload = false)
|
2015-11-05 16:24:21 -05:00
|
|
|
if reload || @sibling_versions.nil?
|
|
|
|
@sibling_versions = self.class.with_item_keys(item_type, item_id)
|
|
|
|
end
|
|
|
|
@sibling_versions
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def next
|
|
|
|
@next ||= sibling_versions.subsequent(self).first
|
|
|
|
end
|
|
|
|
|
|
|
|
def previous
|
|
|
|
@previous ||= sibling_versions.preceding(self).first
|
|
|
|
end
|
|
|
|
|
2015-11-02 00:15:23 -05:00
|
|
|
# Returns an integer representing the chronological position of the
|
|
|
|
# version among its siblings (see `sibling_versions`). The "create" event,
|
|
|
|
# for example, has an index of 0.
|
|
|
|
# @api public
|
2013-12-03 16:00:04 -05:00
|
|
|
def index
|
2015-11-05 16:18:25 -05:00
|
|
|
@index ||= RecordHistory.new(sibling_versions, self.class).index(self)
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
|
2015-11-02 00:02:06 -05:00
|
|
|
# TODO: The `private` method has no effect here. Remove it?
|
|
|
|
# AFAICT it is not possible to have private instance methods in a mixin,
|
|
|
|
# though private *class* methods are possible.
|
2013-12-03 16:00:04 -05:00
|
|
|
private
|
|
|
|
|
2015-11-27 22:29:57 -05:00
|
|
|
# @api private
|
|
|
|
def object_changes_deserialized
|
|
|
|
if self.class.object_changes_col_is_json?
|
|
|
|
object_changes
|
|
|
|
else
|
|
|
|
PaperTrail.serializer.load(object_changes)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-12-03 16:00:04 -05:00
|
|
|
# In Rails 3.1+, calling reify on a previous version confuses the
|
|
|
|
# IdentityMap, if enabled. This prevents insertion into the map.
|
2015-11-02 00:02:06 -05:00
|
|
|
# @api private
|
2013-12-03 16:00:04 -05:00
|
|
|
def without_identity_map(&block)
|
|
|
|
if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
|
|
|
|
::ActiveRecord::IdentityMap.without(&block)
|
|
|
|
else
|
|
|
|
block.call
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-08-03 16:45:42 -04:00
|
|
|
# Checks that a value has been set for the `version_limit` config
|
|
|
|
# option, and if so enforces it.
|
2015-11-02 00:02:06 -05:00
|
|
|
# @api private
|
2013-12-03 16:00:04 -05:00
|
|
|
def enforce_version_limit!
|
2015-11-02 00:09:34 -05:00
|
|
|
limit = PaperTrail.config.version_limit
|
|
|
|
return unless limit.is_a? Numeric
|
2013-12-03 16:00:04 -05:00
|
|
|
previous_versions = sibling_versions.not_creates
|
2015-11-02 00:09:34 -05:00
|
|
|
return unless previous_versions.size > limit
|
|
|
|
excess_versions = previous_versions - previous_versions.last(limit)
|
|
|
|
excess_versions.map(&:destroy)
|
2013-12-03 16:00:04 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|