Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for reifying belongs_to relationships #730

Merged
merged 1 commit into from
Mar 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,8 @@ the association that are created in the same transaction.

To restore Has-One associations as they were at the time, pass option `:has_one
=> true` to `reify`. To restore Has-Many and Has-Many-Through associations, use
option `:has_many => true`. For example:
option `:has_many => true`. To restore Belongs-To association, use
option `:belongs_to => true`. For example:

```ruby
class Location < ActiveRecord::Base
Expand Down
48 changes: 36 additions & 12 deletions lib/paper_trail/reifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def reify(version, options)
mark_for_destruction: false,
has_one: false,
has_many: false,
belongs_to: false,
unversioned_attributes: :nil
)

Expand Down Expand Up @@ -60,15 +61,6 @@ def reify(version, options)

private

def reify_associations(model, options, version)
if options[:has_one]
reify_has_ones version.transaction_id, model, options
end
if options[:has_many]
reify_has_manys version.transaction_id, model, options
end
end

# 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 : {}
Expand Down Expand Up @@ -114,7 +106,7 @@ def prepare_array_for_has_many(array, options, versions)
elsif version.event == "create"
options[:mark_for_destruction] ? record.tap(&:mark_for_destruction) : nil
else
version.reify(options.merge(has_many: false, has_one: false))
version.reify(options.merge(has_many: false, has_one: false, belongs_to: false))
end
end

Expand All @@ -123,7 +115,7 @@ def prepare_array_for_has_many(array, options, versions)
# associations.
array.concat(
versions.values.map { |v|
v.reify(options.merge(has_many: false, has_one: false))
v.reify(options.merge(has_many: false, has_one: false, belongs_to: false))
}
)

Expand All @@ -132,6 +124,14 @@ def prepare_array_for_has_many(array, options, versions)
nil
end

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]
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).
Expand All @@ -156,7 +156,7 @@ def reify_has_ones(transaction_id, model, options = {})
end
end
else
child = version.reify(options.merge(has_many: false, has_one: false))
child = version.reify(options.merge(has_many: false, has_one: false, belongs_to: false))
model.appear_as_new_record do
without_persisting(child) do
model.send "#{assoc.name}=", child
Expand All @@ -166,6 +166,30 @@ def reify_has_ones(transaction_id, model, options = {})
end
end

def reify_belongs_tos(transaction_id, model, options = {})
associations = model.class.reflect_on_all_associations(:belongs_to)

associations.each do |assoc|
next unless assoc.klass.paper_trail_enabled_for_model?
collection_key = model.send(assoc.association_foreign_key)

version = assoc.klass.paper_trail_version_class.
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
version.reify(options.merge(has_many: false, has_one: false,
belongs_to: false))
end

model.send("#{assoc.name}=".to_sym, collection)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_sym isn't necessary here -- send automatically does that for you

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "send" method in ruby v1.9.3 accepts only a symbol.

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.
Expand Down
122 changes: 122 additions & 0 deletions test/unit/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -769,4 +769,126 @@ class AssociationsTest < ActiveSupport::TestCase
end
end
end

context "belongs_to associations" do
context "Wotsit and Widget" do
setup { @widget = Widget.create(name: "widget_0") }

context "where the association is created between model versions" do
setup do
@wotsit = Wotsit.create(name: "wotsit_0")
Timecop.travel 1.second.since
@wotsit.update_attributes widget_id: @widget.id, name: "wotsit_1"
end

context "when reified" do
setup { @wotsit_0 = @wotsit.versions.last.reify(belongs_to: true) }

should "see the associated as it was at the time" do
assert_equal nil, @wotsit_0.widget
end

should "not persist changes to the live association" do
assert_equal @widget, @wotsit.reload.widget
end
end

context "and then the associated is updated between model versions" do
setup do
@widget.update_attributes name: "widget_1"
@widget.update_attributes name: "widget_2"
Timecop.travel 1.second.since
@wotsit.update_attributes name: "wotsit_2"
@widget.update_attributes name: "widget_3"
end

context "when reified" do
setup { @wotsit_1 = @wotsit.versions.last.reify(belongs_to: true) }

should "see the associated as it was at the time" do
assert_equal "widget_2", @wotsit_1.widget.name
end

should "not persist changes to the live association" do
assert_equal "widget_3", @wotsit.reload.widget.name
end
end

context "when reified opting out of belongs_to reification" do
setup { @wotsit_1 = @wotsit.versions.last.reify(belongs_to: false) }

should "see the associated as it is live" do
assert_equal "widget_3", @wotsit_1.widget.name
end
end
end

context "and then the associated is destroyed" do
setup do
@wotsit.update_attributes name: "wotsit_2"
@widget.destroy
end

context "when reified" do
setup { @wotsit_2 = @wotsit.versions.last.reify(belongs_to: true) }

should "see the associated as it was at the time" do
assert_equal @widget, @wotsit_2.widget
end

should "not persist changes to the live association" do
assert_nil @wotsit.reload.widget
end
end

context "and then the model is updated" do
setup do
Timecop.travel 1.second.since
@wotsit.update_attributes name: "wotsit_3"
end

context "when reified" do
setup { @wotsit_2 = @wotsit.versions.last.reify(belongs_to: true) }

should "see the associated as it was the time" do
assert_nil @wotsit_2.widget
end
end
end
end
end

context "where the association is changed between model versions" do
setup do
@wotsit = @widget.create_wotsit(name: "wotsit_0")
Timecop.travel 1.second.since
@new_widget = Widget.create(name: "new_widget")
@wotsit.update_attributes(widget_id: @new_widget.id, name: "wotsit_1")
end

context "when reified" do
setup { @wotsit_0 = @wotsit.versions.last.reify(belongs_to: true) }

should "see the association as it was at the time" do
assert_equal "widget_0", @wotsit_0.widget.name
end

should "not persist changes to the live association" do
assert_equal @new_widget, @wotsit.reload.widget
end
end

context "when reified with option mark_for_destruction" do
setup do
@wotsit_0 = @wotsit.versions.last.
reify(belongs_to: true, mark_for_destruction: true)
end

should "should not mark the new associated for destruction" do
assert_equal false, @new_widget.marked_for_destruction?
end
end
end
end
end
end