2016-05-18 00:32:24 -04:00
|
|
|
require "paper_trail/attribute_serializers/object_attribute"
|
|
|
|
|
2015-08-03 18:37:40 -04:00
|
|
|
module PaperTrail
|
|
|
|
# Given a version record and some options, builds a new model object.
|
|
|
|
# @api private
|
|
|
|
module Reifier
|
|
|
|
class << self
|
|
|
|
# See `VersionConcern#reify` for documentation.
|
|
|
|
# @api private
|
|
|
|
def reify(version, options)
|
2016-04-26 01:42:49 -04:00
|
|
|
options = apply_defaults_to(options, version)
|
2016-04-01 01:42:54 -04:00
|
|
|
attrs = version.object_deserialized
|
2015-08-03 18:37:40 -04:00
|
|
|
|
|
|
|
# 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.
|
|
|
|
if options[:dup] != true && version.item
|
|
|
|
model = version.item
|
|
|
|
if options[:unversioned_attributes] == :nil
|
2016-04-26 01:56:23 -04:00
|
|
|
init_unversioned_attrs(attrs, model)
|
2015-08-03 18:37:40 -04:00
|
|
|
end
|
|
|
|
else
|
2016-03-13 19:20:17 -04:00
|
|
|
klass = version_reification_class(version, attrs)
|
2015-08-03 18:37:40 -04:00
|
|
|
# The `dup` option always returns a new object, otherwise we should
|
|
|
|
# attempt to look for the item outside of default scope(s).
|
2016-02-15 20:09:59 -05:00
|
|
|
if options[:dup] || (item_found = klass.unscoped.find_by_id(version.item_id)).nil?
|
2015-08-03 18:37:40 -04:00
|
|
|
model = klass.new
|
|
|
|
elsif options[:unversioned_attributes] == :nil
|
2016-02-15 20:09:59 -05:00
|
|
|
model = item_found
|
2016-04-26 01:56:23 -04:00
|
|
|
init_unversioned_attrs(attrs, model)
|
2015-08-03 18:37:40 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
reify_attributes(model, version, attrs)
|
2015-08-03 18:37:40 -04:00
|
|
|
model.send "#{model.class.version_association_name}=", version
|
2016-03-13 19:23:15 -04:00
|
|
|
reify_associations(model, options, version)
|
|
|
|
model
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
2015-08-03 18:37:40 -04:00
|
|
|
|
2016-04-26 01:42:49 -04:00
|
|
|
# Given a hash of `options` for `.reify`, return a new hash with default
|
|
|
|
# values applied.
|
|
|
|
# @api private
|
|
|
|
def apply_defaults_to(options, version)
|
|
|
|
{
|
|
|
|
version_at: version.created_at,
|
|
|
|
mark_for_destruction: false,
|
|
|
|
has_one: false,
|
|
|
|
has_many: false,
|
|
|
|
belongs_to: false,
|
|
|
|
has_and_belongs_to_many: false,
|
|
|
|
unversioned_attributes: :nil
|
|
|
|
}.merge(options)
|
|
|
|
end
|
|
|
|
|
2016-04-21 01:28:58 -04:00
|
|
|
# @api private
|
|
|
|
def each_enabled_association(associations)
|
|
|
|
associations.each do |assoc|
|
2016-02-23 17:59:14 -05:00
|
|
|
next unless assoc.klass.paper_trail.enabled?
|
2016-04-21 01:28:58 -04:00
|
|
|
yield assoc
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-04-21 01:52:33 -04:00
|
|
|
# Examine the `source_reflection`, i.e. the "source" of `assoc` the
|
|
|
|
# `ThroughReflection`. The source can be a `BelongsToReflection`
|
|
|
|
# or a `HasManyReflection`.
|
|
|
|
#
|
|
|
|
# If the association is a has_many association again, then call
|
|
|
|
# reify_has_manys for each record in `through_collection`.
|
|
|
|
#
|
|
|
|
# @api private
|
|
|
|
def hmt_collection(through_collection, assoc, options, transaction_id)
|
|
|
|
if !assoc.source_reflection.belongs_to? && through_collection.present?
|
|
|
|
hmt_collection_through_has_many(
|
|
|
|
through_collection, assoc, options, transaction_id
|
|
|
|
)
|
|
|
|
else
|
|
|
|
hmt_collection_through_belongs_to(
|
|
|
|
through_collection, assoc, options, transaction_id
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @api private
|
|
|
|
def hmt_collection_through_has_many(through_collection, assoc, options, transaction_id)
|
|
|
|
through_collection.each do |through_model|
|
|
|
|
reify_has_manys(transaction_id, through_model, options)
|
|
|
|
end
|
|
|
|
|
|
|
|
# At this point, the "through" part of the association chain has
|
|
|
|
# been reified, but not the final, "target" part. To continue our
|
|
|
|
# example, `model.sections` (including `model.sections.paragraphs`)
|
|
|
|
# has been loaded. However, the final "target" part of the
|
|
|
|
# association, that is, `model.paragraphs`, has not been loaded. So,
|
|
|
|
# we do that now.
|
|
|
|
through_collection.flat_map { |through_model|
|
|
|
|
through_model.public_send(assoc.name.to_sym).to_a
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
# @api private
|
|
|
|
def hmt_collection_through_belongs_to(through_collection, assoc, options, transaction_id)
|
|
|
|
collection_keys = through_collection.map { |through_model|
|
|
|
|
through_model.send(assoc.source_reflection.foreign_key)
|
|
|
|
}
|
2016-02-23 17:59:14 -05:00
|
|
|
version_id_subquery = assoc.klass.paper_trail.version_class.
|
2016-04-21 01:52:33 -04:00
|
|
|
select("MIN(id)").
|
|
|
|
where("item_type = ?", assoc.class_name).
|
|
|
|
where("item_id IN (?)", collection_keys).
|
|
|
|
where(
|
|
|
|
"created_at >= ? OR transaction_id = ?",
|
|
|
|
options[:version_at],
|
|
|
|
transaction_id
|
|
|
|
).
|
|
|
|
group("item_id").
|
|
|
|
to_sql
|
|
|
|
versions = versions_by_id(assoc.klass, version_id_subquery)
|
|
|
|
collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
|
|
|
|
prepare_array_for_has_many(collection, options, versions)
|
|
|
|
collection
|
|
|
|
end
|
|
|
|
|
2016-04-26 01:56:23 -04:00
|
|
|
# Look for attributes that exist in `model` and not in this version.
|
|
|
|
# These attributes should be set to nil. Modifies `attrs`.
|
|
|
|
# @api private
|
|
|
|
def init_unversioned_attrs(attrs, model)
|
|
|
|
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
|
|
|
|
end
|
|
|
|
|
2016-04-26 01:22:53 -04:00
|
|
|
# Given a HABTM association `assoc` and an `id`, return a version record
|
|
|
|
# from the point in time identified by `transaction_id` or `version_at`.
|
|
|
|
# @api private
|
|
|
|
def load_version_for_habtm(assoc, id, transaction_id, version_at)
|
2016-02-23 17:59:14 -05:00
|
|
|
assoc.klass.paper_trail.version_class.
|
2016-04-26 01:22:53 -04:00
|
|
|
where("item_type = ?", assoc.klass.name).
|
|
|
|
where("item_id = ?", id).
|
|
|
|
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
|
|
|
order("id").
|
|
|
|
limit(1).
|
|
|
|
first
|
|
|
|
end
|
|
|
|
|
2016-04-26 00:43:38 -04:00
|
|
|
# Given a has-one association `assoc` on `model`, return the version
|
|
|
|
# record from the point in time identified by `transaction_id` or `version_at`.
|
|
|
|
# @api private
|
|
|
|
def load_version_for_has_one(assoc, model, transaction_id, version_at)
|
2016-02-23 17:59:14 -05:00
|
|
|
version_table_name = model.class.paper_trail.version_class.table_name
|
|
|
|
model.class.paper_trail.version_class.joins(:version_associations).
|
2016-04-26 00:43:38 -04:00
|
|
|
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 = ?", version_at, transaction_id).
|
|
|
|
order("#{version_table_name}.id ASC").
|
|
|
|
first
|
|
|
|
end
|
|
|
|
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
# Set all the attributes in this version on the model.
|
|
|
|
def reify_attributes(model, version, attrs)
|
|
|
|
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
|
2016-05-18 00:32:24 -04:00
|
|
|
AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
attrs.each do |k, v|
|
2016-05-18 00:32:24 -04:00
|
|
|
# `ObjectAttribute#deserialize` will return the mapped enum value
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
# and in Rails < 5, the []= uses the integer type caster from the column
|
|
|
|
# definition (in general) and thus will turn a (usually) string to 0 instead
|
|
|
|
# of the correct value
|
2016-03-13 14:51:26 -04:00
|
|
|
is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
|
Maps enums to database values before storing in `object_changes`
Keep consistency between versions with regard to `changes` and
`object_changes` and how enum columns store their values.
Before, `changes` would map the changed attributes enum columns to the database
values (integer values). This allows reifying that version to maintain the
integrity of the enum. It did not do so for `object_changes` and thus, `0`
for non-json columns, and the enum value for json columns would be stored instead.
For the non-json columns, it mapped any non-integer enum value to `0` because
during serialization that column is an `integer`. Now this is fixed,
so that `object_changes` stores the enum mapped value.
Here is an example:
```ruby
class PostWithStatus < ActiveRecord::Base
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
end
post = PostWithStatus.new(status: :draft)
post.published!
version = post.versions.last
# Before
version.changeset #> { 'status' => ['draft', 'draft'] } (stored as [0, 0])
# After
version.changeset #> { 'status' => ['draft', 'published'] } (stored as [0, 1])
```
2016-03-11 04:18:40 -05:00
|
|
|
|
|
|
|
if model.has_attribute?(k) && !is_enum_without_type_caster
|
|
|
|
model[k.to_sym] = v
|
|
|
|
elsif model.respond_to?("#{k}=")
|
|
|
|
model.send("#{k}=", v)
|
|
|
|
else
|
|
|
|
version.logger.warn(
|
|
|
|
"Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-10-08 19:04:24 -04:00
|
|
|
# Replaces each record in `array` with its reified version, if present
|
|
|
|
# in `versions`.
|
|
|
|
#
|
|
|
|
# @api private
|
|
|
|
# @param array - The collection to be modified.
|
|
|
|
# @param options
|
|
|
|
# @param versions - A `Hash` mapping IDs to `Version`s
|
|
|
|
# @return nil - Always returns `nil`
|
|
|
|
#
|
|
|
|
# Once modified by this method, `array` will be assigned to the
|
|
|
|
# AR association currently being reified.
|
|
|
|
#
|
|
|
|
def prepare_array_for_has_many(array, options, versions)
|
|
|
|
# Iterate each child to replace it with the previous value if there is
|
|
|
|
# a version after the timestamp.
|
|
|
|
array.map! do |record|
|
|
|
|
if (version = versions.delete(record.id)).nil?
|
|
|
|
record
|
2016-03-05 17:07:32 -05:00
|
|
|
elsif version.event == "create"
|
2016-03-05 17:00:08 -05:00
|
|
|
options[:mark_for_destruction] ? record.tap(&:mark_for_destruction) : nil
|
2015-10-08 19:04:24 -04:00
|
|
|
else
|
2016-04-26 01:35:42 -04:00
|
|
|
version.reify(
|
|
|
|
options.merge(
|
|
|
|
has_many: false,
|
|
|
|
has_one: false,
|
|
|
|
belongs_to: false,
|
|
|
|
has_and_belongs_to_many: false
|
|
|
|
)
|
|
|
|
)
|
2015-10-08 19:04:24 -04:00
|
|
|
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.
|
|
|
|
array.concat(
|
|
|
|
versions.values.map { |v|
|
2016-04-26 01:35:42 -04:00
|
|
|
v.reify(
|
|
|
|
options.merge(
|
|
|
|
has_many: false,
|
|
|
|
has_one: false,
|
|
|
|
belongs_to: false,
|
|
|
|
has_and_belongs_to_many: false
|
|
|
|
)
|
|
|
|
)
|
2015-10-08 19:04:24 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
array.compact!
|
|
|
|
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2016-03-09 14:41:21 -05:00
|
|
|
def reify_associations(model, options, version)
|
|
|
|
reify_has_ones version.transaction_id, model, options if options[:has_one]
|
|
|
|
|
|
|
|
reify_belongs_tos version.transaction_id, model, options if options[:belongs_to]
|
|
|
|
|
|
|
|
reify_has_manys version.transaction_id, model, options if options[:has_many]
|
2016-04-05 05:37:24 -04:00
|
|
|
|
|
|
|
if options[:has_and_belongs_to_many]
|
|
|
|
reify_has_and_belongs_to_many version.transaction_id, model, options
|
|
|
|
end
|
2016-03-09 14:41:21 -05:00
|
|
|
end
|
|
|
|
|
2015-08-03 18:37:40 -04:00
|
|
|
# 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(transaction_id, model, options = {})
|
2016-04-21 01:28:58 -04:00
|
|
|
associations = model.class.reflect_on_all_associations(:has_one)
|
|
|
|
each_enabled_association(associations) do |assoc|
|
2016-04-26 00:43:38 -04:00
|
|
|
version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
|
2016-03-05 17:27:33 -05:00
|
|
|
next unless version
|
|
|
|
if version.event == "create"
|
|
|
|
if options[:mark_for_destruction]
|
|
|
|
model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
|
|
|
|
else
|
2016-02-23 17:59:14 -05:00
|
|
|
model.paper_trail.appear_as_new_record do
|
2016-03-05 17:27:33 -05:00
|
|
|
model.send "#{assoc.name}=", nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
2016-04-26 01:35:42 -04:00
|
|
|
child = version.reify(
|
|
|
|
options.merge(
|
|
|
|
has_many: false,
|
|
|
|
has_one: false,
|
|
|
|
belongs_to: false,
|
|
|
|
has_and_belongs_to_many: false
|
|
|
|
)
|
|
|
|
)
|
2016-02-23 17:59:14 -05:00
|
|
|
model.paper_trail.appear_as_new_record do
|
2016-03-05 17:27:33 -05:00
|
|
|
without_persisting(child) do
|
|
|
|
model.send "#{assoc.name}=", child
|
2015-08-03 18:37:40 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-03-09 14:41:21 -05:00
|
|
|
def reify_belongs_tos(transaction_id, model, options = {})
|
|
|
|
associations = model.class.reflect_on_all_associations(:belongs_to)
|
2016-04-21 01:28:58 -04:00
|
|
|
each_enabled_association(associations) do |assoc|
|
2016-03-09 14:41:21 -05:00
|
|
|
collection_key = model.send(assoc.association_foreign_key)
|
2016-02-23 17:59:14 -05:00
|
|
|
version = assoc.klass.paper_trail.version_class.
|
2016-03-09 14:41:21 -05:00
|
|
|
where("item_type = ?", assoc.class_name).
|
|
|
|
where("item_id = ?", collection_key).
|
|
|
|
where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
|
|
|
|
order("id").limit(1).first
|
|
|
|
|
|
|
|
collection = if version.nil?
|
|
|
|
assoc.klass.where(assoc.klass.primary_key => collection_key).first
|
|
|
|
else
|
2016-04-26 01:35:42 -04:00
|
|
|
version.reify(
|
|
|
|
options.merge(
|
|
|
|
has_many: false,
|
|
|
|
has_one: false,
|
|
|
|
belongs_to: false,
|
|
|
|
has_and_belongs_to_many: false
|
|
|
|
)
|
|
|
|
)
|
2016-03-09 14:41:21 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
model.send("#{assoc.name}=".to_sym, collection)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-08-03 18:37:40 -04:00
|
|
|
# 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(transaction_id, model, options = {})
|
|
|
|
assoc_has_many_through, assoc_has_many_directly =
|
|
|
|
model.class.reflect_on_all_associations(:has_many).
|
|
|
|
partition { |assoc| assoc.options[:through] }
|
|
|
|
reify_has_many_directly(transaction_id, assoc_has_many_directly, model, options)
|
|
|
|
reify_has_many_through(transaction_id, assoc_has_many_through, model, options)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Restore the `model`'s has_many associations not associated through
|
|
|
|
# another association.
|
|
|
|
def reify_has_many_directly(transaction_id, associations, model, options = {})
|
2016-02-23 17:59:14 -05:00
|
|
|
version_table_name = model.class.paper_trail.version_class.table_name
|
2016-04-21 01:28:58 -04:00
|
|
|
each_enabled_association(associations) do |assoc|
|
2015-08-03 18:37:40 -04:00
|
|
|
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
|
2015-10-08 19:54:33 -04:00
|
|
|
versions = versions_by_id(model.class, version_id_subquery)
|
2016-01-11 22:03:20 -05:00
|
|
|
collection = Array.new model.send(assoc.name).reload # to avoid cache
|
2015-10-08 19:04:24 -04:00
|
|
|
prepare_array_for_has_many(collection, options, versions)
|
|
|
|
model.send(assoc.name).proxy_association.target = collection
|
2015-08-03 18:37:40 -04:00
|
|
|
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).
|
|
|
|
def reify_has_many_through(transaction_id, associations, model, options = {})
|
2016-04-21 01:28:58 -04:00
|
|
|
each_enabled_association(associations) do |assoc|
|
2015-08-03 18:37:40 -04:00
|
|
|
# Load the collection of through-models. For example, if `model` is a
|
|
|
|
# Chapter, having many Paragraphs through Sections, then
|
|
|
|
# `through_collection` will contain Sections.
|
|
|
|
through_collection = model.send(assoc.options[:through])
|
|
|
|
|
2016-04-21 01:52:33 -04:00
|
|
|
# Now, given the collection of "through" models (e.g. sections), load
|
|
|
|
# the collection of "target" models (e.g. paragraphs)
|
|
|
|
collection = hmt_collection(through_collection, assoc, options, transaction_id)
|
2015-10-12 03:27:24 -04:00
|
|
|
|
2016-04-21 01:52:33 -04:00
|
|
|
# Finally, assign the `collection` of "target" models, e.g. to
|
|
|
|
# `model.paragraphs`.
|
2015-10-12 03:27:24 -04:00
|
|
|
model.send(assoc.name).proxy_association.target = collection
|
2015-08-03 18:37:40 -04:00
|
|
|
end
|
|
|
|
end
|
2015-10-08 19:54:33 -04:00
|
|
|
|
2016-04-05 05:37:24 -04:00
|
|
|
def reify_has_and_belongs_to_many(transaction_id, model, options = {})
|
2016-04-26 01:05:22 -04:00
|
|
|
model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
|
2016-02-23 17:59:14 -05:00
|
|
|
papertrail_enabled = assoc.klass.paper_trail.enabled?
|
2016-04-05 05:37:24 -04:00
|
|
|
next unless
|
2016-04-26 01:05:22 -04:00
|
|
|
model.class.paper_trail_save_join_tables.include?(assoc.name) ||
|
2016-04-05 05:37:24 -04:00
|
|
|
papertrail_enabled
|
|
|
|
|
|
|
|
version_ids = PaperTrail::VersionAssociation.
|
2016-04-26 01:05:22 -04:00
|
|
|
where("foreign_key_name = ?", assoc.name).
|
2016-04-05 05:37:24 -04:00
|
|
|
where("version_id = ?", transaction_id).
|
|
|
|
pluck(:foreign_key_id)
|
|
|
|
|
2016-04-26 01:05:22 -04:00
|
|
|
model.send(assoc.name).proxy_association.target =
|
2016-04-05 05:37:24 -04:00
|
|
|
version_ids.map do |id|
|
|
|
|
if papertrail_enabled
|
2016-04-26 01:22:53 -04:00
|
|
|
version = load_version_for_habtm(
|
|
|
|
assoc,
|
|
|
|
id,
|
|
|
|
transaction_id,
|
|
|
|
options[:version_at]
|
|
|
|
)
|
2016-04-05 05:37:24 -04:00
|
|
|
if version
|
2016-04-26 01:35:42 -04:00
|
|
|
next version.reify(
|
|
|
|
options.merge(
|
|
|
|
has_many: false,
|
|
|
|
has_one: false,
|
|
|
|
belongs_to: false,
|
|
|
|
has_and_belongs_to_many: false
|
|
|
|
)
|
|
|
|
)
|
2016-04-05 05:37:24 -04:00
|
|
|
end
|
|
|
|
end
|
2016-04-26 01:05:22 -04:00
|
|
|
assoc.klass.where(assoc.klass.primary_key => id).first
|
2016-04-05 05:37:24 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-03-13 19:20:17 -04:00
|
|
|
# Given a `version`, return the class to reify. This method supports
|
2016-04-06 18:29:45 -04:00
|
|
|
# Single Table Inheritance (STI) with custom inheritance columns.
|
|
|
|
#
|
|
|
|
# For example, imagine a `version` whose `item_type` is "Animal". The
|
|
|
|
# `animals` table is an STI table (it has cats and dogs) and it has a
|
|
|
|
# custom inheritance column, `species`. If `attrs["species"]` is "Dog",
|
|
|
|
# this method returns the constant `Dog`. If `attrs["species"]` is blank,
|
|
|
|
# this method returns the constant `Animal`. You can see this particular
|
|
|
|
# example in action in `spec/models/animal_spec.rb`.
|
|
|
|
#
|
2016-03-13 19:20:17 -04:00
|
|
|
def version_reification_class(version, attrs)
|
|
|
|
inheritance_column_name = version.item_type.constantize.inheritance_column
|
2016-04-01 01:42:54 -04:00
|
|
|
inher_col_value = attrs[inheritance_column_name]
|
|
|
|
class_name = inher_col_value.blank? ? version.item_type : inher_col_value
|
2016-03-13 19:20:17 -04:00
|
|
|
class_name.constantize
|
|
|
|
end
|
|
|
|
|
2015-10-08 19:54:33 -04:00
|
|
|
# Given a SQL fragment that identifies the IDs of version records,
|
|
|
|
# returns a `Hash` mapping those IDs to `Version`s.
|
|
|
|
#
|
|
|
|
# @api private
|
|
|
|
# @param klass - An ActiveRecord class.
|
|
|
|
# @param version_id_subquery - String. A SQL subquery that selects
|
|
|
|
# the IDs of version records.
|
|
|
|
# @return A `Hash` mapping IDs to `Version`s
|
|
|
|
#
|
|
|
|
def versions_by_id(klass, version_id_subquery)
|
|
|
|
klass.
|
2016-02-23 17:59:14 -05:00
|
|
|
paper_trail.version_class.
|
2015-10-08 19:54:33 -04:00
|
|
|
where("id IN (#{version_id_subquery})").
|
2016-03-05 17:13:22 -05:00
|
|
|
inject({}) { |a, e| a.merge!(e.item_id => e) }
|
2015-10-08 19:54:33 -04:00
|
|
|
end
|
2016-01-11 22:07:57 -05:00
|
|
|
|
|
|
|
# Temporarily suppress #save so we can reassociate with the reified
|
|
|
|
# master of a has_one relationship. Since ActiveRecord 5 the related
|
|
|
|
# object is saved when it is assigned to the association. ActiveRecord
|
|
|
|
# 5 also happens to be the first version that provides #suppress.
|
|
|
|
def without_persisting(record)
|
|
|
|
if record.class.respond_to? :suppress
|
|
|
|
record.class.suppress { yield }
|
|
|
|
else
|
|
|
|
yield
|
|
|
|
end
|
|
|
|
end
|
2015-08-03 18:37:40 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|