mirror of
https://github.com/paper-trail-gem/paper_trail.git
synced 2022-11-09 11:33:19 -05:00
Merge branch 'master' of https://github.com/lyfeyaj/paper_trail into merge-with-lyfeyaj
Conflicts: lib/paper_trail.rb lib/paper_trail/has_paper_trail.rb lib/paper_trail/version_concern.rb
This commit is contained in:
commit
ce2cf0d234
8 changed files with 218 additions and 33 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,3 +15,5 @@ Gemfile.lock
|
|||
vendor/*
|
||||
.idea
|
||||
.rvmrc
|
||||
.tags
|
||||
.tags_sorted_by_file
|
||||
|
|
|
@ -11,12 +11,23 @@ module PaperTrail
|
|||
desc 'Generates (but does not run) a migration to add a versions table.'
|
||||
|
||||
def create_migration_file
|
||||
migration_template 'create_versions.rb', 'db/migrate/create_versions.rb'
|
||||
migration_template 'add_object_changes_to_versions.rb', 'db/migrate/add_object_changes_to_versions.rb' if options.with_changes?
|
||||
add_paper_trail_migration('create_versions')
|
||||
add_paper_trail_migration('add_object_changes_to_versions') if options.with_changes?
|
||||
add_paper_trail_migration('create_version_associations')
|
||||
add_paper_trail_migration('add_transaction_id_column_to_versions')
|
||||
end
|
||||
|
||||
def self.next_migration_number(dirname)
|
||||
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
||||
end
|
||||
|
||||
protected
|
||||
def add_paper_trail_migration(template)
|
||||
migration_dir = File.expand_path('db/migrate')
|
||||
|
||||
if !self.class.migration_exists?(migration_dir, template)
|
||||
migration_template "#{template}.rb", "db/migrate/#{template}.rb"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class AddTransactionIdColumnToVersions < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :versions, :transaction_id, :integer
|
||||
add_index :versions, [:transaction_id]
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :versions, :transaction_id
|
||||
remove_index :versions, [:transaction_id]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
class CreateVersionAssociations < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :version_associations do |t|
|
||||
t.integer :version_id
|
||||
t.string :foreign_key_name, :null => false
|
||||
t.integer :foreign_key_id
|
||||
end
|
||||
add_index :version_associations, [:version_id]
|
||||
add_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_on_foreign_key_name_and foreign_key_id'
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_index :version_associations, [:version_id]
|
||||
remove_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_on_foreign_key_name_and foreign_key_id'
|
||||
drop_table :version_associations
|
||||
end
|
||||
end
|
|
@ -94,6 +94,28 @@ module PaperTrail
|
|||
@active_record_protected_attributes ||= ::ActiveRecord::VERSION::MAJOR < 4 || !!defined?(ProtectedAttributes)
|
||||
end
|
||||
|
||||
def self.transaction?
|
||||
ActiveRecord::Base.connection.open_transactions > 0 || paper_trail_store[:transaction_open]
|
||||
end
|
||||
|
||||
def self.start_transaction
|
||||
paper_trail_store[:transaction_open] = true
|
||||
self.transaction_id = nil
|
||||
end
|
||||
|
||||
def self.end_transaction
|
||||
paper_trail_store[:transaction_open] = false
|
||||
self.transaction_id = nil
|
||||
end
|
||||
|
||||
def self.transaction_id
|
||||
paper_trail_store[:transaction_id]
|
||||
end
|
||||
|
||||
def self.transaction_id=(id)
|
||||
paper_trail_store[:transaction_id] = id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Thread-safe hash to hold PaperTrail's data.
|
||||
|
@ -120,6 +142,9 @@ unless PaperTrail.active_record_protected_attributes?
|
|||
rescue LoadError; end # will rescue if `ProtectedAttributes` gem is not available
|
||||
end
|
||||
|
||||
require 'paper_trail/version'
|
||||
require 'paper_trail/version_association'
|
||||
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
include PaperTrail::Model
|
||||
end
|
||||
|
|
|
@ -78,6 +78,10 @@ module PaperTrail
|
|||
after_update :clear_version_instance!
|
||||
end
|
||||
after_destroy :record_destroy, :if => :save_version? if options_on.empty? || options_on.include?(:destroy)
|
||||
|
||||
# Reset the transaction id when the transaction is closed
|
||||
after_commit :reset_transaction_id
|
||||
after_rollback :reset_transaction_id
|
||||
end
|
||||
|
||||
# Switches PaperTrail off for this class.
|
||||
|
@ -223,6 +227,16 @@ 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
|
||||
def appear_as_new_record
|
||||
instance_eval {
|
||||
alias :old_new_record? :new_record?
|
||||
alias :new_record? :present?
|
||||
}
|
||||
yield
|
||||
instance_eval { alias :new_record? :old_new_record? }
|
||||
end
|
||||
|
||||
# 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?
|
||||
|
@ -258,8 +272,9 @@ module PaperTrail
|
|||
def record_create
|
||||
if paper_trail_switched_on?
|
||||
data = {
|
||||
:event => paper_trail_event || 'create',
|
||||
:whodunnit => PaperTrail.whodunnit
|
||||
:event => paper_trail_event || 'create',
|
||||
:whodunnit => PaperTrail.whodunnit,
|
||||
:transaction_id => PaperTrail.transaction_id
|
||||
}
|
||||
if respond_to?(:created_at)
|
||||
data[PaperTrail.timestamp_field] = created_at
|
||||
|
@ -268,7 +283,9 @@ module PaperTrail
|
|||
data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
|
||||
PaperTrail.serializer.dump(changes_for_paper_trail)
|
||||
end
|
||||
send(self.class.versions_association_name).create! merge_metadata(data)
|
||||
version = send(self.class.versions_association_name).create! merge_metadata(data)
|
||||
set_transaction_id(version)
|
||||
save_associations(version)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -276,9 +293,10 @@ module PaperTrail
|
|||
if paper_trail_switched_on? && changed_notably?
|
||||
object_attrs = object_attrs_for_paper_trail(item_before_change)
|
||||
data = {
|
||||
:event => paper_trail_event || 'update',
|
||||
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
|
||||
:whodunnit => PaperTrail.whodunnit
|
||||
:event => paper_trail_event || 'update',
|
||||
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
|
||||
:whodunnit => PaperTrail.whodunnit,
|
||||
:transaction_id => PaperTrail.transaction_id
|
||||
}
|
||||
if respond_to?(:updated_at)
|
||||
data[PaperTrail.timestamp_field] = updated_at
|
||||
|
@ -287,7 +305,9 @@ module PaperTrail
|
|||
data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
|
||||
PaperTrail.serializer.dump(changes_for_paper_trail)
|
||||
end
|
||||
send(self.class.versions_association_name).create merge_metadata(data)
|
||||
version = send(self.class.versions_association_name).create merge_metadata(data)
|
||||
set_transaction_id(version)
|
||||
save_associations(version)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -311,17 +331,43 @@ module PaperTrail
|
|||
if paper_trail_switched_on? and not new_record?
|
||||
object_attrs = object_attrs_for_paper_trail(item_before_change)
|
||||
data = {
|
||||
:item_id => self.id,
|
||||
:item_type => self.class.base_class.name,
|
||||
:event => paper_trail_event || 'destroy',
|
||||
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
|
||||
:whodunnit => PaperTrail.whodunnit
|
||||
:item_id => self.id,
|
||||
:item_type => self.class.base_class.name,
|
||||
:event => paper_trail_event || 'destroy',
|
||||
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
|
||||
:whodunnit => PaperTrail.whodunnit,
|
||||
:transaction_id => PaperTrail.transaction_id
|
||||
}
|
||||
send("#{self.class.version_association_name}=", self.class.paper_trail_version_class.create(merge_metadata(data)))
|
||||
version = self.class.paper_trail_version_class.create(merge_metadata(data))
|
||||
send("#{self.class.version_association_name}=", version)
|
||||
send(self.class.versions_association_name).send :load_target
|
||||
set_transaction_id(version)
|
||||
save_associations(version)
|
||||
end
|
||||
end
|
||||
|
||||
def save_associations(version)
|
||||
self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
||||
PaperTrail::VersionAssociation.create(
|
||||
:version_id => version.id,
|
||||
:foreign_key_name => assoc.foreign_key,
|
||||
:foreign_key_id => self.send(assoc.foreign_key)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def set_transaction_id(version)
|
||||
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
|
||||
PaperTrail.transaction_id = version.id
|
||||
version.transaction_id = version.id
|
||||
version.save
|
||||
end
|
||||
end
|
||||
|
||||
def reset_transaction_id
|
||||
PaperTrail.transaction_id = nil
|
||||
end
|
||||
|
||||
def merge_metadata(data)
|
||||
# First we merge the model-level metadata in `meta`.
|
||||
paper_trail_options[:meta].each do |k,v|
|
||||
|
|
7
lib/paper_trail/version_association.rb
Normal file
7
lib/paper_trail/version_association.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module PaperTrail
|
||||
class VersionAssociation < ActiveRecord::Base
|
||||
belongs_to :version
|
||||
|
||||
attr_accessible :version_id, :foreign_key_name, :foreign_key_id if PaperTrail.active_record_protected_attributes?
|
||||
end
|
||||
end
|
|
@ -6,9 +6,15 @@ module PaperTrail
|
|||
|
||||
included do
|
||||
belongs_to :item, :polymorphic => true
|
||||
has_many :version_associations, :dependent => :destroy
|
||||
|
||||
validates_presence_of :event
|
||||
attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes, :created_at if PaperTrail.active_record_protected_attributes?
|
||||
|
||||
attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes, :transaction_id, :created_at if PaperTrail.active_record_protected_attributes?
|
||||
|
||||
after_create :enforce_version_limit!
|
||||
|
||||
scope :within_transaction, lambda { |id| where :transaction_id => id }
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
@ -116,8 +122,11 @@ module PaperTrail
|
|||
return nil if object.nil?
|
||||
|
||||
without_identity_map do
|
||||
options[:has_one] = 3 if options[:has_one] == true
|
||||
options.reverse_merge! :has_one => false
|
||||
options.reverse_merge!(
|
||||
:version_at => created_at,
|
||||
:has_one => false,
|
||||
:has_many => false
|
||||
)
|
||||
|
||||
attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
|
||||
|
||||
|
@ -158,8 +167,16 @@ module PaperTrail
|
|||
|
||||
model.send "#{model.class.version_association_name}=", self
|
||||
|
||||
# unless options[:has_one] == false
|
||||
# reify_has_ones model, options[:has_one]
|
||||
# end
|
||||
|
||||
unless options[:has_one] == false
|
||||
reify_has_ones model, options[:has_one]
|
||||
reify_has_ones model, options
|
||||
end
|
||||
|
||||
unless options[:has_many] == false
|
||||
reify_has_manys model, options
|
||||
end
|
||||
|
||||
model
|
||||
|
@ -179,6 +196,15 @@ module PaperTrail
|
|||
{}
|
||||
end
|
||||
|
||||
# Rollback all changes within a transaction
|
||||
def rollback
|
||||
transaction do
|
||||
self.class.within_transaction(transaction_id).reverse_each do |version|
|
||||
version.reify.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns who put the item into the state stored in this version.
|
||||
def originator
|
||||
@originator ||= previous.whodunnit rescue nil
|
||||
|
@ -230,28 +256,68 @@ module PaperTrail
|
|||
# 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).
|
||||
#
|
||||
# The `lookback` sets how many seconds before the model's change we go.
|
||||
def reify_has_ones(model, lookback)
|
||||
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|
|
||||
child = model.send assoc.name
|
||||
if child.respond_to? :version_at
|
||||
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
|
||||
# Ideally we want the version of the child as it was just before the parent was updated...
|
||||
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
|
||||
# updated on the same form), it's impossible to tell when the overall update started;
|
||||
# and therefore impossible to know when "just before" was.
|
||||
if (child_as_it_was = child.version_at(send(PaperTrail.timestamp_field) - lookback.seconds))
|
||||
child_as_it_was.attributes.each do |k,v|
|
||||
model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
|
||||
version = model.class.paper_trail_version_class.joins(:version_associations).
|
||||
where("version_associations.foreign_key_name = ?", assoc.foreign_key).
|
||||
where("version_associations.foreign_key_id = ?", model.id).
|
||||
where("#{version_table_name}.item_type = ?", assoc.class_name).
|
||||
where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
|
||||
order("#{version_table_name}.id ASC").first
|
||||
if version
|
||||
if version.event == 'create'
|
||||
if child = version.item
|
||||
child.mark_for_destruction
|
||||
model.send "#{assoc.name}=", nil
|
||||
end
|
||||
else
|
||||
model.send "#{assoc.name}=", nil
|
||||
child = version.reify options
|
||||
logger.info "Reify #{child}"
|
||||
model.appear_as_new_record do
|
||||
model.send "#{assoc.name}=", child
|
||||
end
|
||||
end
|
||||
end
|
||||
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.
|
||||
def reify_has_manys(model, options = {})
|
||||
version_table_name = model.class.paper_trail_version_class.table_name
|
||||
model.class.reflect_on_all_associations(:has_many).each do |assoc|
|
||||
next if assoc.name == model.class.versions_association_name
|
||||
version_id_subquery = PaperTrail::VersionAssociation.joins(model.class.version_association_name).
|
||||
select("MIN(version_id)").
|
||||
where("foreign_key_name = ?", assoc.foreign_key).
|
||||
where("foreign_key_id = ?", model.id).
|
||||
where("#{version_table_name}.item_type = ?", assoc.class_name).
|
||||
where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
|
||||
group("item_id").to_sql
|
||||
versions = model.class.paper_trail_version_class.where("id IN (#{version_id_subquery})")
|
||||
|
||||
# Pass true to force the model to load
|
||||
collection = Array.new model.send(assoc.name, true)
|
||||
|
||||
# Iterate all the child records to replace them with the previous values
|
||||
versions.each do |version|
|
||||
collection << version.reify(options) if version.event == 'destroy'
|
||||
collection.map! do |c|
|
||||
if version.event == 'create'
|
||||
c.mark_for_destruction if version.item && version.item.id == c.id
|
||||
c
|
||||
else
|
||||
child = version.reify(options)
|
||||
c.id == child.id ? child : c
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
model.send(assoc.name).proxy_association.target = collection
|
||||
end
|
||||
end
|
||||
|
||||
# checks to see if 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
|
||||
|
|
Loading…
Reference in a new issue