Merge pull request #582 from airblade/formatting_comments

Format comments
This commit is contained in:
Jared Beck 2015-08-03 19:12:53 -04:00
commit b5c2bce93d
7 changed files with 222 additions and 147 deletions

View File

@ -25,7 +25,7 @@ module PaperTrail
end
# ActiveRecord 5 drops support for serialized attributes; for previous
# versions of ActiveRecord it is supported, we have a config option
# versions of ActiveRecord it is supported, we have a config option
# to enable it within PaperTrail.
def self.serialized_attributes?
!!PaperTrail.config.serialized_attributes && ::ActiveRecord::VERSION::MAJOR < 5
@ -43,12 +43,14 @@ module PaperTrail
!!paper_trail_store[:request_enabled_for_controller]
end
# Sets whether PaperTrail is enabled or disabled for this model in the current request.
# Sets whether PaperTrail is enabled or disabled for this model in the
# current request.
def self.enabled_for_model(model, value)
paper_trail_store[:"enabled_for_#{model}"] = value
end
# Returns `true` if PaperTrail is enabled for this model in the current request, `false` otherwise.
# Returns `true` if PaperTrail is enabled for this model in the current
# request, `false` otherwise.
def self.enabled_for_model?(model)
!!paper_trail_store.fetch(:"enabled_for_#{model}", true)
end
@ -63,10 +65,9 @@ module PaperTrail
PaperTrail.config.timestamp_field
end
# Sets who is responsible for any changes that occur.
# You would normally use this in a migration or on the console,
# when working with models directly. In a controller it is set
# automatically to the `current_user`.
# Sets who is responsible for any changes that occur. You would normally use
# this in a migration or on the console, when working with models directly.
# In a controller it is set automatically to the `current_user`.
def self.whodunnit=(value)
paper_trail_store[:whodunnit] = value
end
@ -76,8 +77,8 @@ module PaperTrail
paper_trail_store[:whodunnit]
end
# Sets any information from the controller that you want PaperTrail
# to store. By default this is set automatically by a before filter.
# Sets any information from the controller that you want PaperTrail to
# store. By default this is set automatically by a before filter.
def self.controller_info=(value)
paper_trail_store[:controller_info] = value
end
@ -117,8 +118,8 @@ module PaperTrail
private
# Thread-safe hash to hold PaperTrail's data.
# Initializing with needed default values.
# Thread-safe hash to hold PaperTrail's data. Initializing with needed
# default values.
def self.paper_trail_store
RequestStore.store[:paper_trail] ||= { :request_enabled_for_controller => true }
end
@ -135,12 +136,15 @@ module PaperTrail
end
end
# Ensure `ProtectedAttributes` gem gets required if it is available before the `Version` class gets loaded in
# Ensure `ProtectedAttributes` gem gets required if it is available before the
# `Version` class gets loaded in.
unless PaperTrail.active_record_protected_attributes?
PaperTrail.send(:remove_instance_variable, :@active_record_protected_attributes)
begin
require 'protected_attributes'
rescue LoadError; end # will rescue if `ProtectedAttributes` gem is not available
rescue LoadError
# In case `ProtectedAttributes` gem is not available.
end
end
ActiveSupport.on_load(:active_record) do

View File

@ -1,19 +1,24 @@
module PaperTrail
module Cleaner
# Destroys all but the most recent version(s) for items on a given date (or on all dates). Useful for deleting drafts.
# Destroys all but the most recent version(s) for items on a given date
# (or on all dates). Useful for deleting drafts.
#
# Options:
# :keeping An `integer` indicating the number of versions to be kept for each item per date.
# Defaults to `1`.
# :date Should either be a `Date` object specifying which date to destroy versions for or `:all`,
# which will specify that all dates should be cleaned. Defaults to `:all`.
# :item_id The `id` for the item to be cleaned on, or `nil`, which causes all items to be cleaned.
# Defaults to `nil`.
#
# - :keeping - An `integer` indicating the number of versions to be kept for
# each item per date. Defaults to `1`.
# - :date - Should either be a `Date` object specifying which date to
# destroy versions for or `:all`, which will specify that all dates
# should be cleaned. Defaults to `:all`.
# - :item_id - The `id` for the item to be cleaned on, or `nil`, which
# causes all items to be cleaned. Defaults to `nil`.
#
def clean_versions!(options = {})
options = {:keeping => 1, :date => :all}.merge(options)
gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }.each do |date, _versions|
# remove the number of versions we wish to keep from the collection of versions prior to destruction
# Remove the number of versions we wish to keep from the collection
# of versions prior to destruction.
_versions.pop(options[:keeping])
_versions.map(&:destroy)
end
@ -22,13 +27,18 @@ module PaperTrail
private
# Returns a hash of versions grouped by the `item_id` attribute formatted like this: {:item_id => PaperTrail::Version}.
# If `item_id` or `date` is set, versions will be narrowed to those pointing at items with those ids that were created on specified date.
# Returns a hash of versions grouped by the `item_id` attribute formatted
# like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
# set, versions will be narrowed to those pointing at items with those ids
# that were created on specified date.
def gather_versions(item_id = nil, date = :all)
raise ArgumentError.new("`date` argument must receive a Timestamp or `:all`") unless date == :all || date.respond_to?(:to_date)
versions = item_id ? PaperTrail::Version.where(:item_id => item_id) : PaperTrail::Version
versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
versions = PaperTrail::Version.all if versions == PaperTrail::Version # if versions has not been converted to an ActiveRecord::Relation yet, do so now
# If `versions` has not been converted to an ActiveRecord::Relation yet,
# do so now.
versions = PaperTrail::Version.all if versions == PaperTrail::Version
versions.group_by(&:item_id)
end
end

View File

@ -1,4 +1,4 @@
# This file only needs to be loaded if the gem is being used outside of Rails, since otherwise
# the model(s) will get loaded in via the `Rails::Engine`
# This file only needs to be loaded if the gem is being used outside of Rails,
# since otherwise the model(s) will get loaded in via the `Rails::Engine`.
require "paper_trail/frameworks/active_record/models/paper_trail/version_association"
require "paper_trail/frameworks/active_record/models/paper_trail/version"

View File

@ -36,24 +36,26 @@ module PaperTrail
#
# The columns `ip` and `user_agent` must exist in your `versions` # table.
#
# Use the `:meta` option to `PaperTrail::Model::ClassMethods.has_paper_trail`
# to store any extra model-level data you need.
# Use the `:meta` option to
# `PaperTrail::Model::ClassMethods.has_paper_trail` to store any extra
# model-level data you need.
def info_for_paper_trail
{}
end
# Returns `true` (default) or `false` depending on whether PaperTrail should
# be active for the current request.
# Returns `true` (default) or `false` depending on whether PaperTrail
# should be active for the current request.
#
# Override this method in your controller to specify when PaperTrail should
# be off.
# Override this method in your controller to specify when PaperTrail
# should be off.
def paper_trail_enabled_for_controller
::PaperTrail.enabled?
end
private
# Tells PaperTrail whether versions should be saved in the current request.
# Tells PaperTrail whether versions should be saved in the current
# request.
def set_paper_trail_enabled_for_controller
::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
end
@ -63,8 +65,8 @@ module PaperTrail
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
end
# Tells PaperTrail any information from the controller you want
# to store alongside any changes that occur.
# Tells PaperTrail any information from the controller you want to store
# alongside any changes that occur.
def set_paper_trail_controller_info
::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
end

View File

@ -3,7 +3,8 @@ require 'active_support/core_ext/object' # provides the `try` method
module PaperTrail
module Sinatra
# Register this module inside your Sinatra application to gain access to controller-level methods used by PaperTrail
# Register this module inside your Sinatra application to gain access to
# controller-level methods used by PaperTrail.
def self.registered(app)
app.use RequestStore::Middleware
app.helpers self

View File

@ -8,31 +8,42 @@ module PaperTrail
end
module ClassMethods
# Declare this in your model to track every create, update, and destroy. Each version of
# the model is available in the `versions` association.
# Declare this in your model to track every create, update, and destroy.
# Each version of the model is available in the `versions` association.
#
# Options:
# :on the events to track (optional; defaults to all of them). Set to an array of
# `:create`, `:update`, `:destroy` as desired.
# :class_name the name of a custom Version class. This class should inherit from `PaperTrail::Version`.
# :ignore an array of attributes for which a new `Version` will not be created if only they change.
# it can also aceept a Hash as an argument where the key is the attribute to ignore (a `String` or `Symbol`),
# which will only be ignored if the value is a `Proc` which returns truthily.
# :if, :unless Procs that allow to specify conditions when to save versions for an object
# :only inverse of `ignore` - a new `Version` will be created only for these attributes if supplied
# it can also aceept a Hash as an argument where the key is the attribute to track (a `String` or `Symbol`),
# which will only be counted if the value is a `Proc` which returns truthily.
# :skip fields to ignore completely. As with `ignore`, updates to these fields will not create
# a new `Version`. In addition, these fields will not be included in the serialized versions
# of the object whenever a new `Version` is created.
# :meta a hash of extra data to store. You must add a column to the `versions` table for each key.
# Values are objects or procs (which are called with `self`, i.e. the model with the paper
# trail). See `PaperTrail::Controller.info_for_paper_trail` for how to store data from
# the controller.
# :versions the name to use for the versions association. Default is `:versions`.
# :version the name to use for the method which returns the version the instance was reified from.
# Default is `:version`.
# :save_changes whether or not to save changes to the object_changes column if it exists. Default is true
#
# - :on - The events to track (optional; defaults to all of them). Set
# to an array of `:create`, `:update`, `:destroy` as desired.
# - :class_name - The name of a custom Version class. This class should
# inherit from `PaperTrail::Version`.
# - :ignore - An array of attributes for which a new `Version` will not be
# created if only they change. It can also aceept a Hash as an
# argument where the key is the attribute to ignore (a `String` or
# `Symbol`), which will only be ignored if the value is a `Proc` which
# returns truthily.
# - :if, :unless - Procs that allow to specify conditions when to save
# versions for an object.
# - :only - Inverse of `ignore`. A new `Version` will be created only
# for these attributes if supplied it can also aceept a Hash as an
# argument where the key is the attribute to track (a `String` or
# `Symbol`), which will only be counted if the value is a `Proc` which
# returns truthily.
# - :skip - Fields to ignore completely. As with `ignore`, updates to
# these fields will not create a new `Version`. In addition, these
# fields will not be included in the serialized versions of the object
# whenever a new `Version` is created.
# - :meta - A hash of extra data to store. You must add a column to the
# `versions` table for each key. Values are objects or procs (which
# are called with `self`, i.e. the model with the paper trail). See
# `PaperTrail::Controller.info_for_paper_trail` for how to store data
# from the controller.
# - :versions - The name to use for the versions association. Default
# is `:versions`.
# - :version - The name to use for the method which returns the version
# the instance was reified from. Default is `:version`.
# - :save_changes - Whether or not to save changes to the object_changes
# column if it exists. Default is true
#
def has_paper_trail(options = {})
# Lazily include the instance methods so we don't clutter up
@ -64,7 +75,8 @@ module PaperTrail
attr_accessor :paper_trail_event
if ::ActiveRecord::VERSION::MAJOR >= 4 # `has_many` syntax for specifying order uses a lambda in Rails 4
# `has_many` syntax for specifying order uses a lambda in Rails 4
if ::ActiveRecord::VERSION::MAJOR >= 4
has_many self.versions_association_name,
lambda { order(model.timestamp_sort_order) },
:class_name => self.version_class_name, :as => :item
@ -76,7 +88,11 @@ module PaperTrail
end
options[:on] ||= [:create, :update, :destroy]
options_on = Array(options[:on]) # so that a single symbol can be passed in without wrapping it in an `Array`
# Wrap the :on option in an array if necessary. This allows a single
# symbol to be passed in.
options_on = Array(options[:on])
after_create :record_create, :if => :save_version? if options_on.include?(:create)
if options_on.include?(:update)
before_save :reset_timestamp_attrs_for_update_if_needed!, :on => :update
@ -85,7 +101,7 @@ module PaperTrail
end
after_destroy :record_destroy, :if => :save_version? if options_on.include?(:destroy)
# Reset the transaction id when the transaction is closed
# Reset the transaction id when the transaction is closed.
after_commit :reset_transaction_id
after_rollback :reset_transaction_id
after_rollback :clear_rolled_back_versions
@ -110,40 +126,47 @@ module PaperTrail
@paper_trail_version_class ||= version_class_name.constantize
end
# Used for Version#object attribute
# Used for `Version#object` attribute.
def serialize_attributes_for_paper_trail!(attributes)
# don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return attributes if self.paper_trail_version_class.object_col_is_json?
serialized_attributes.each do |key, coder|
if attributes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method
# Fall back to current serializer if `coder` has no `dump` method.
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
attributes[key] = coder.dump(attributes[key])
end
end
end
# TODO: There is a lot of duplication between this and
# `serialize_attributes_for_paper_trail!`.
def unserialize_attributes_for_paper_trail!(attributes)
# don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return attributes if self.paper_trail_version_class.object_col_is_json?
serialized_attributes.each do |key, coder|
if attributes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method.
# TODO: Shouldn't this be `:load`?
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
attributes[key] = coder.load(attributes[key])
end
end
end
# Used for Version#object_changes attribute
# Used for Version#object_changes attribute.
def serialize_attribute_changes_for_paper_trail!(changes)
# don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
# Don't serialize before values before inserting into columns of type `JSON`
# on `PostgreSQL` databases.
return changes if self.paper_trail_version_class.object_changes_col_is_json?
serialized_attributes.each do |key, coder|
if changes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method
# Fall back to current serializer if `coder` has no `dump` method.
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
old_value, new_value = changes[key]
changes[key] = [coder.dump(old_value),
@ -152,12 +175,17 @@ module PaperTrail
end
end
# TODO: There is a lot of duplication between this and
# `serialize_attribute_changes_for_paper_trail!`.
def unserialize_attribute_changes_for_paper_trail!(changes)
# don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return changes if self.paper_trail_version_class.object_changes_col_is_json?
serialized_attributes.each do |key, coder|
if changes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method.
# TODO: Shouldn't this be `:load`?
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
old_value, new_value = changes[key]
changes[key] = [coder.load(old_value),
@ -236,7 +264,8 @@ module PaperTrail
self.class.paper_trail_on! if paper_trail_was_enabled
end
# Utility method for reifying. Anything executed inside the block will appear like a new record
# Utility method for reifying. Anything executed inside the block will
# appear like a new record.
def appear_as_new_record
instance_eval {
alias :old_new_record? :new_record?
@ -246,7 +275,8 @@ module PaperTrail
instance_eval { alias :new_record? :old_new_record? }
end
# Temporarily overwrites the value of whodunnit and then executes the provided block.
# Temporarily overwrites the value of whodunnit and then executes the
# provided block.
def whodunnit(value)
raise ArgumentError, 'expected to receive a block' unless block_given?
current_whodunnit = PaperTrail.whodunnit
@ -344,15 +374,19 @@ module PaperTrail
_changes.to_hash
end
# Invoked via`after_update` callback for when a previous version is reified and then saved
# Invoked via`after_update` callback for when a previous version is
# reified and then saved.
def clear_version_instance!
send("#{self.class.version_association_name}=", nil)
end
# Invoked via callback when a user attempts to persist a reified
# `Version`.
def reset_timestamp_attrs_for_update_if_needed!
return if self.live? # invoked via callback when a user attempts to persist a reified `Version`
return if self.live?
timestamp_attributes_for_update_in_model.each do |column|
# ActiveRecord 4.2 deprecated `reset_column!` in favor of `restore_column!`
# ActiveRecord 4.2 deprecated `reset_column!` in favor of
# `restore_column!`.
if respond_to?("restore_#{column}!")
send("restore_#{column}!")
else
@ -382,7 +416,7 @@ module PaperTrail
end
end
# saves associations if the join table for `VersionAssociation` exists
# Saves associations if the join table for `VersionAssociation` exists.
def save_associations(version)
return unless PaperTrail.config.track_associations?
self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
@ -424,8 +458,8 @@ module PaperTrail
if v.respond_to?(:call)
v.call(self)
elsif v.is_a?(Symbol) && respond_to?(v)
# if it is an attribute that is changing in an existing object,
# be sure to grab the current version
# If it is an attribute that is changing in an existing object,
# be sure to grab the current version.
if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] != 'create'
send("#{v}_was".to_sym)
else
@ -435,6 +469,7 @@ module PaperTrail
v
end
end
# Second we merge any extra data from the controller (if available).
data.merge(PaperTrail.controller_info || {})
end
@ -449,8 +484,8 @@ module PaperTrail
end
end
# returns hash of attributes (with appropriate attributes serialized),
# ommitting attributes to be skipped
# Returns hash of attributes (with appropriate attributes serialized),
# ommitting attributes to be skipped.
def object_attrs_for_paper_trail(attributes_hash)
attrs = attributes_hash.except(*self.paper_trail_options[:skip])
if PaperTrail.serialized_attributes?
@ -459,10 +494,9 @@ module PaperTrail
attrs
end
# This method determines whether it is appropriate to generate a new
# version instance. A timestamp-only update (e.g. only `updated_at`
# changed) is considered notable unless an ignored attribute was also
# changed.
# Determines whether it is appropriate to generate a new version
# instance. A timestamp-only update (e.g. only `updated_at` changed) is
# considered notable unless an ignored attribute was also changed.
def changed_notably?
if ignored_attr_has_changed?
timestamps = timestamp_attributes_for_update_in_model.map(&:to_s)
@ -473,8 +507,8 @@ module PaperTrail
end
# An attributed is "ignored" if it is listed in the `:ignore` option
# and/or the `:skip` option. Returns true if an ignored attribute
# has changed.
# and/or the `:skip` option. Returns true if an ignored attribute has
# changed.
def ignored_attr_has_changed?
ignored = self.paper_trail_options[:ignore] + self.paper_trail_options[:skip]
ignored.any? && (changed & ignored).any?
@ -482,7 +516,8 @@ module PaperTrail
def notably_changed
only = self.paper_trail_options[:only].dup
# remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
# Remove Hash arguments and then evaluate whether the attributes (the
# keys of the hash) should also get pushed into the collection.
only.delete_if do |obj|
obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
end
@ -491,7 +526,8 @@ module PaperTrail
def changed_and_not_ignored
ignore = self.paper_trail_options[:ignore].dup
# remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
# Remove Hash arguments and then evaluate whether the attributes (the
# keys of the hash) should also get pushed into the collection.
ignore.delete_if do |obj|
obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
end

View File

@ -7,10 +7,10 @@ module PaperTrail
included do
belongs_to :item, :polymorphic => true
# 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/`)
# 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/`).
if PaperTrail.config.track_associations?
has_many :version_associations, :dependent => :destroy
end
@ -47,8 +47,8 @@ module PaperTrail
where 'event <> ?', 'create'
end
# Expects `obj` to be an instance of `PaperTrail::Version` by default, but can accept a timestamp if
# `timestamp_arg` receives `true`
# Expects `obj` to be an instance of `PaperTrail::Version` by default,
# but can accept a timestamp if `timestamp_arg` receives `true`
def subsequent(obj, timestamp_arg = false)
if timestamp_arg != true && self.primary_key_is_int?
return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
@ -75,7 +75,8 @@ module PaperTrail
).order(self.timestamp_sort_order)
end
# defaults to using the primary key as the secondary sort order if possible
# Defaults to using the primary key as the secondary sort order if
# possible.
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?
@ -137,12 +138,14 @@ module PaperTrail
true
end
# Returns whether the `object` column is using the `json` type supported by PostgreSQL
# Returns whether the `object` column is using the `json` type supported
# by PostgreSQL.
def object_col_is_json?
[:json, :jsonb].include?(columns_hash['object'].type)
end
# Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL
# Returns whether the `object_changes` column is using the `json` type
# supported by PostgreSQL.
def object_changes_col_is_json?
[:json, :jsonb].include?(columns_hash['object_changes'].try(:type))
end
@ -150,22 +153,31 @@ module PaperTrail
# Restore the item from this version.
#
# 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.
# 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.
#
# Options:
# :has_one set to `true` to also reify has_one associations. Default is `false`.
# :has_many set to `true` to also reify has_many and has_many :through associations.
# Default is `false`.
# :mark_for_destruction set to `true` to mark the has_one/has_many associations that did not exist in the
# reified version for destruction, instead of remove them. Default is `false`.
# This option is handy for people who want to persist the reified version.
# :dup `false` default behavior
# `true` it always create a new object instance. It is 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
#
# - :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.
#
def reify(options = {})
return nil if object.nil?
@ -180,22 +192,22 @@ module PaperTrail
attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
# Normally a polymorphic belongs_to relationship allows us
# to get the object we belong to by calling, in this case,
# `item`. However this returns nil if `item` has been
# destroyed, and we need to be able to retrieve destroyed
# objects.
# Normally a polymorphic belongs_to relationship allows us to get the
# object we belong to by calling, in this case, `item`. However this
# returns nil if `item` has been destroyed, and we need to be able to
# retrieve destroyed objects.
#
# In this situation we constantize the `item_type` to get hold of
# the class...except when the stored object's attributes
# include a `type` key. If this is the case, the object
# we belong to is using single table inheritance and the
# `item_type` will be the base class, not the actual subclass.
# If `type` is present but empty, the class is the base class.
# In this situation we constantize the `item_type` to get hold of the
# class...except when the stored object's attributes include a `type`
# key. If this is the case, the object we belong to is using single
# table inheritance and the `item_type` will be the base class, not the
# actual subclass. If `type` is present but empty, the class is the base
# class.
if options[:dup] != true && item
model = item
# Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
# Look for attributes that exist in the model and not in this
# version. These attributes should be set to nil.
if options[:unversioned_attributes] == :nil
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
end
@ -203,13 +215,14 @@ module PaperTrail
inheritance_column_name = item_type.constantize.inheritance_column
class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
klass = class_name.constantize
# the `dup` option always returns a new object, otherwise we should attempt
# to look for the item outside of default scope(s)
# The `dup` option always returns a new object, otherwise we should
# attempt to look for the item outside of default scope(s).
if options[:dup] || (_item = klass.unscoped.find_by_id(item_id)).nil?
model = klass.new
elsif options[:unversioned_attributes] == :nil
model = _item
# Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
# Look for attributes that exist in the model and not in this
# version. These attributes should be set to nil.
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
end
end
@ -218,7 +231,7 @@ module PaperTrail
model.class.unserialize_attributes_for_paper_trail! attrs
end
# Set all the attributes in this version on the model
# Set all the attributes in this version on the model.
attrs.each do |k, v|
if model.has_attribute?(k)
model[k.to_sym] = v
@ -243,8 +256,9 @@ module PaperTrail
end
end
# 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.
# 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.
def changeset
return nil unless self.class.column_names.include? 'object_changes'
@ -268,8 +282,8 @@ module PaperTrail
self.paper_trail_originator
end
# Returns who changed the item from the state it had in this version.
# This is an alias for `whodunnit`.
# Returns who changed the item from the state it had in this version. This
# is an alias for `whodunnit`.
def terminator
@terminator ||= whodunnit
end
@ -311,9 +325,9 @@ module PaperTrail
end
end
# Restore the `model`'s has_one associations as they were when this version was
# superseded by the next (because that's what the user was looking at when they
# made the change).
# Restore the `model`'s has_one associations as they were when this
# version was superseded by the next (because that's what the user was
# looking at when they made the change).
def reify_has_ones(model, options = {})
version_table_name = model.class.paper_trail_version_class.table_name
model.class.reflect_on_all_associations(:has_one).each do |assoc|
@ -344,8 +358,9 @@ module PaperTrail
end
end
# Restore the `model`'s has_many associations as they were at version_at timestamp
# We lookup the first child versions after version_at timestamp or in same transaction.
# Restore the `model`'s has_many associations as they were at version_at
# timestamp We lookup the first child versions after version_at timestamp or
# in same transaction.
def reify_has_manys(model, options = {})
assoc_has_many_through, assoc_has_many_directly =
model.class.reflect_on_all_associations(:has_many).
@ -354,7 +369,8 @@ module PaperTrail
reify_has_many_through(assoc_has_many_through, model, options)
end
# Restore the `model`'s has_many associations not associated through another association
# Restore the `model`'s has_many associations not associated through
# another association.
def reify_has_many_directly(associations, model, options = {})
version_table_name = model.class.paper_trail_version_class.table_name
associations.each do |assoc|
@ -370,10 +386,11 @@ module PaperTrail
acc.merge!(v.item_id => v)
end
# Pass true to force the model to load
# Pass true to force the model to load.
collection = Array.new model.send(assoc.name, true)
# Iterate each child to replace it with the previous value if there is a version after the timestamp
# Iterate each child to replace it with the previous value if there is
# a version after the timestamp.
collection.map! do |c|
if (version = versions.delete(c.id)).nil?
c
@ -384,16 +401,18 @@ module PaperTrail
end
end
# Reify the rest of the versions and add them to the collection, these versions are for those that
# have been removed from the live associations
# Reify the rest of the versions and add them to the collection, these
# versions are for those that have been removed from the live
# associations.
collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
model.send(assoc.name).proxy_association.target = collection.compact
end
end
# Restore the `model`'s has_many associations through another association
# This must be called after the direct has_manys have been reified (reify_has_many_directly)
# Restore the `model`'s has_many associations through another association.
# This must be called after the direct has_manys have been reified
# (reify_has_many_directly).
def reify_has_many_through(associations, model, options = {})
associations.each do |assoc|
next unless assoc.klass.paper_trail_enabled_for_model?
@ -412,7 +431,8 @@ module PaperTrail
collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
# Iterate each child to replace it with the previous value if there is a version after the timestamp
# Iterate each child to replace it with the previous value if there is
# a version after the timestamp.
collection.map! do |c|
if (version = versions.delete(c.id)).nil?
c
@ -423,15 +443,17 @@ module PaperTrail
end
end
# Reify the rest of the versions and add them to the collection, these versions are for those that
# have been removed from the live associations
# Reify the rest of the versions and add them to the collection, these
# versions are for those that have been removed from the live
# associations.
collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
model.send(assoc.name).proxy_association.target = collection.compact
end
end
# checks to see if a value has been set for the `version_limit` config option, and if so enforces it
# Checks that a value has been set for the `version_limit` config
# option, and if so enforces it.
def enforce_version_limit!
return unless PaperTrail.config.version_limit.is_a? Numeric
previous_versions = sibling_versions.not_creates