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:
NullVoxPopuli 2014-10-20 13:04:51 -04:00
commit ce2cf0d234
8 changed files with 218 additions and 33 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ Gemfile.lock
vendor/*
.idea
.rvmrc
.tags
.tags_sorted_by_file

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View 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

View File

@ -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