Automatically restore has_one associations.
This commit is contained in:
parent
734b450037
commit
9728e3158e
22
README.md
22
README.md
|
@ -11,6 +11,7 @@ PaperTrail lets you track changes to your models' data. It's good for auditing
|
|||
* Allows you to get at every version, including the original, even once destroyed.
|
||||
* Allows you to get at every version even if the schema has since changed.
|
||||
* Allows you to get at the version as of a particular time.
|
||||
* Automatically restores the `has_one` associations as they were at the time.
|
||||
* Automatically records who was responsible via your controller. PaperTrail calls `current_user` by default, if it exists, but you can have it call any method you like.
|
||||
* Allows you to set who is responsible at model-level (useful for migrations).
|
||||
* Allows you to store arbitrary model-level metadata with each version (useful for filtering versions).
|
||||
|
@ -212,6 +213,27 @@ To find out who made a `version`'s object look that way, use `version.originator
|
|||
>> last_version.terminator # 'Bob'
|
||||
|
||||
|
||||
## Has-One Associations
|
||||
|
||||
PaperTrail automatically restores `:has_one` associations as they were at the time.
|
||||
|
||||
class Treasure < ActiveRecord::Base
|
||||
has_one :location
|
||||
end
|
||||
|
||||
>> treasure.amount # 100
|
||||
>> treasure.location.latitude # 12.345
|
||||
|
||||
>> treasure.update_attributes :amount => 153
|
||||
>> treasure.location.update_attributes :latitude => 54.321
|
||||
|
||||
>> t = treasure.versions.last.reify
|
||||
>> t.amount # 100
|
||||
>> t.location.latitude # 12.345
|
||||
|
||||
Unfortunately PaperTrail doesn't do this for `:has_many` associations (I can't get it to work) or `:belongs_to` (I ran out of time looking at `:has_many`).
|
||||
|
||||
|
||||
## Has-Many-Through Associations
|
||||
|
||||
PaperTrail can track most changes to the join table. Specifically it can track all additions but it can only track removals which fire the `after_destroy` callback on the join table. Here are some examples:
|
||||
|
|
|
@ -85,14 +85,10 @@ module PaperTrail
|
|||
|
||||
# Returns the object (not a Version) as it was at the given timestamp.
|
||||
def version_at(timestamp)
|
||||
# Short-circuit if the current state is applicable.
|
||||
return self if self.updated_at <= timestamp
|
||||
# Look for the first version created after, rather than before, the
|
||||
# timestamp because a version stores how the object looked before the
|
||||
# change.
|
||||
version = versions.first :conditions => ['created_at > ?', timestamp],
|
||||
:order => 'created_at ASC'
|
||||
version.try :reify
|
||||
# Because a version stores how its object looked *before* the change,
|
||||
# we need to look for the first version created *after* the timestamp.
|
||||
version = versions.first :conditions => ['created_at > ?', timestamp], :order => 'created_at ASC, id ASC'
|
||||
version ? version.reify : self
|
||||
end
|
||||
|
||||
# Returns the object (not a Version) as it was most recently.
|
||||
|
@ -109,6 +105,17 @@ module PaperTrail
|
|||
subsequent_version.reify if subsequent_version
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Returns the object (not a Version) as it was until the version record
|
||||
# with the given id.
|
||||
def version_until(id)
|
||||
# Because a version stores how its object looked *before* the change,
|
||||
# we need to look for the first version created *on or after* the id.
|
||||
version = versions.first :conditions => ['id >= ?', id], :order => 'id ASC'
|
||||
version ? version.reify : self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def merge_metadata(data)
|
||||
|
|
|
@ -36,6 +36,10 @@ class Version < ActiveRecord::Base
|
|||
end
|
||||
|
||||
model.version = self
|
||||
# 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).
|
||||
reify_has_ones model
|
||||
model
|
||||
end
|
||||
end
|
||||
|
@ -66,4 +70,21 @@ class Version < ActiveRecord::Base
|
|||
:order => 'id ASC').index(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reify_has_ones(model)
|
||||
model.class.reflect_on_all_associations(:has_one).each do |assoc|
|
||||
child = model.send assoc.name
|
||||
if child.respond_to? :version_until
|
||||
if (version_until = child.version_until(id))
|
||||
version_until.attributes.each do |k,v|
|
||||
model.send(assoc.name).send "#{k}=", v rescue nil
|
||||
end
|
||||
else
|
||||
model.send "#{assoc.name}=", nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class FooWidget < Widget
|
|||
end
|
||||
|
||||
class Wotsit < ActiveRecord::Base
|
||||
has_paper_trail
|
||||
belongs_to :widget
|
||||
end
|
||||
|
||||
|
@ -45,7 +46,8 @@ end
|
|||
|
||||
class HasPaperTrailModelTest < Test::Unit::TestCase
|
||||
load_schema
|
||||
|
||||
=begin
|
||||
=end
|
||||
context 'A record' do
|
||||
setup { @article = Article.create }
|
||||
|
||||
|
@ -126,7 +128,8 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
should 'copy the has_one association when reifying' do
|
||||
assert_equal @wotsit, @reified_widget.wotsit
|
||||
assert_nil @reified_widget.wotsit # wotsit wasn't there at the last version
|
||||
assert_equal @wotsit, @widget.wotsit # wotsit came into being on the live object
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -644,4 +647,61 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
context 'A model with a has_one association' do
|
||||
setup { @widget = Widget.create :name => 'widget_0' }
|
||||
context 'before the associated was created' do
|
||||
setup do
|
||||
@widget.update_attributes :name => 'widget_1'
|
||||
@wotsit = @widget.create_wotsit :name => 'wotsit_0'
|
||||
end
|
||||
context 'when reified' do
|
||||
setup { @widget_0 = @widget.versions.last.reify }
|
||||
should 'see the associated as it was at the time' do
|
||||
assert_nil @widget_0.wotsit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'where the associated is created between model versions' do
|
||||
setup do
|
||||
@wotsit = @widget.create_wotsit :name => 'wotsit_0'
|
||||
@widget.update_attributes :name => 'widget_1'
|
||||
end
|
||||
context 'when reified' do
|
||||
setup { @widget_0 = @widget.versions.last.reify }
|
||||
should 'see the associated as it was at the time' do
|
||||
assert_equal 'wotsit_0', @widget_0.wotsit.name
|
||||
end
|
||||
end
|
||||
|
||||
context 'and then the associated is updated between model versions' do
|
||||
setup do
|
||||
@wotsit.update_attributes :name => 'wotsit_1'
|
||||
@wotsit.update_attributes :name => 'wotsit_2'
|
||||
@widget.update_attributes :name => 'widget_2'
|
||||
end
|
||||
context 'when reified' do
|
||||
setup { @widget_1 = @widget.versions.last.reify }
|
||||
should 'see the associated as it was at the time' do
|
||||
assert_equal 'wotsit_2', @widget_1.wotsit.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'and then the associated is destroyed between model versions' do
|
||||
setup do
|
||||
@wotsit.destroy
|
||||
@widget.update_attributes :name => 'widget_3'
|
||||
end
|
||||
context 'when reified' do
|
||||
setup { @widget_2 = @widget.versions.last.reify }
|
||||
should 'see the associated as it was at the time' do
|
||||
assert_nil @widget_2.wotsit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -37,6 +37,7 @@ ActiveRecord::Schema.define(:version => 0) do
|
|||
create_table :wotsits, :force => true do |t|
|
||||
t.integer :widget_id
|
||||
t.string :name
|
||||
t.datetime :created_at, :updated_at
|
||||
end
|
||||
|
||||
create_table :fluxors, :force => true do |t|
|
||||
|
|
Loading…
Reference in New Issue