Revisioning with acts_as_audited
When I first created acts_as_audited over a year ago, I intended to add versioning/revisioning capabilities, but found I never really had a need. Well, it just so happened that I finally had a need on an app that we are working on. So, acts_as_audited now allows you to revert back to previous revisions.
You can get all the revisions of a model:
article.revisions.each |revision| puts revision.class #=> Article puts revision.version #=> 7, 6, 5, 4... end
or a specific revision:
revision = article.revision(5) puts revision.title #=> "Old Title"
or the previous revision. Reverting is as simple as saving a revision:
previous = article.revision(:previous) previous.save # revert to previous revision
See the original post for more details about installing and using the plugin.
How is this different from acts_as_versioned?
I’ve never used acts_as_versioned1 (because I’ve never had a need to do versioning), but I do know that it has a few annoying limitations: 1) it requires a separate table (and model) for each model that you want to version, and 2) it saves every attribute, even if it’s not changed.
acts_as_audited already stories all the changes (and only the changes) in one table, so adding versioning was rather trivial. Revisions are created by walking backward through the audits, collecting the changes, and returning an instance of the model.
Upgrading
To use the versioning support, you must add a version field in the audits table:
add_column :audits, :version, :integer, :default => 0
Note: if you already have audit records, the version field will have to be initialized.
Feedback
This code is fairly specific to what I needed, so if you’re using it, I would love to hear how it is working for you. Suggestions and patches welcome.
- acts_as_audited was actually one of the first plugins that I wrote, so I looked at the best code I could find as an example for writing a plugin, which happened to be acts_as_versioned. Kudos to technoweenie.
25 Comments
That’s funny.. I just looked over your old blog post and thought I could use this… :-)
Thanks for your work.
w00t! Thanks very much. I commented on the original blog post about adding this feature. I will test it out as soon as I get a chance.
Sigh, just an update. I got bogged down in my project (wife is pregnant, I have a good excuse) and wasn’t able to test the revisioning yet. But hopefully someone has… :)
Hello, I’m trying to use acts_as_audited with ActiveScaffold (http://activescaffold.com) but for some reason, it pukes in the “after” filter in the caching.
If I comment out line 602 in vendor/rails/actionpack/lib/action_controller/filters.rb it works great, but I’m guessing this is bad since cached objects won’t be invalidated or something…
Any suggestions?
Here’s the relevant exception:
NoMethodError (You have a nil object when you didn’t expect it!
The error occurred while evaluating nil.controller_name):
/vendor/rails/actionpack/lib/action_controller/caching.rb:602:in `callback’
/vendor/rails/actionpack/lib/action_controller/caching.rb:595:in `after’
/vendor/rails/actionpack/lib/action_controller/filters.rb:602:in `proxy_before_and_after_filter’
Hey this is cool! =)
Would be nice if you could retrieve a revision by date as well, since the audit table holds a created_at.
Would also love this to co-operate better with Globalize model translations.
A great plugin. Unfortunately it seems to have suffered on Edge rails as a result of the decision to make acts_as_list a plugin… even after installing the acts_as_list plugin one has to comment out line 14 of audit.rb in order to get the server to start…
Is there an easy fix? Thanks.
I am trying to test the moves from one revision to the other in my functional tests and it seems like the models are not keeping all the changes, what am I supposed to do to make the test works?
This is a great addition to my rails/ruby arsenal! I was wondering if anyone has developed a controller and view to display all of the revisions and possibly revert back to one?
Ryan,
I’ll try to clean up the one that I have and post it. Stay tuned.
Hi Brandon,
Thanks for the plugin. It is exactly what I was looking for.
I was looking for information of maintaining an audit trail for a project I am working on and I stumbled upon your previous blog post.
Regards
Surat
Is there a way to only revert certain attributes? If we have updated first and last name, can I accept the changed last name but not first name?
Ryan,
Not really. You could retrieve a revision and just manually set the attributes. If you’d like to submit a patch, I’d be happy to accept it.
Hi Brandon,
Great plugin, thanks.
I just wonder how could I get the updated date/time of each revision?
e.g. i would like to show a revision history, then i have:
for rev in @article.revisions
puts rev.version
puts rev.updated_at
end
but then updated_at always show the last revision’s timestamp.
I vote to add Peter’s diff, which further enhances acts_as_audited to save a record’s attributes when it is deleted, and gives you an easy way to restore it, with the original id (e.g., Client.revive(2344)).
I’m getting the following error when calling the
method on an audited object.Am I the only one with this problem?
I’m surprised that acts_as_audited was your plugin. It’ll probably make a guest appearance here at Bluefish.
I found that when loggin destroy the latest values are not being saved to the database, hence to re-build the record to its latest state would require you to trail from create to destroy, which can be time consuming. To overcome this I changed:
def audit_destroy(user = nil)
write_audit(:action => ‘destroy’, :user => user)
end
TO:
def audit_destroy(user = nil)
write_audit(:action => ‘destroy’, :changes => audited_attributes, :user => user)
end
is acts_as_audited does not work with date type fields .like i have show_date column in my table . when i make except => [:show_date] then only it works . else it shows error
please help I need to audit datetype fields also
like TypeError (wrong argument type String (expected YAML::JvYAML::Node)):
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/connection_adapters/abstract/quoting.rb:31:in `quote’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/connection_adapters/mysql_adapter.rb:224:in `quote’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2594:in `attributes_with_quotes’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2585:in `each’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2585:in `attributes_with_quotes’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2507:in `create’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/callbacks.rb:220:in `create_with_callbacks’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/timestamp.rb:29:in `create_with_timestamps’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2483:in `create_or_update’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/callbacks.rb:207:in `create_or_update_with_callbacks’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2211:in `save’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/validations.rb:911:in `save_with_validation’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/dirty.rb:75:in `save_with_dirty’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/transactions.rb:106:in `save_with_transactions’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/connection_adapters/abstract/database_statements.rb:66:in `transaction’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/transactions.rb:79:in `transaction’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/transactions.rb:98:in `transaction’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/transactions.rb:106:in `save_with_transactions’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/transactions.rb:118:in `rollback_active_record_state!’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/transactions.rb:106:in `save_with_transactions’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/associations/association_collection.rb:174:in `create’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/associations/association_collection.rb:321:in `create_record’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/associations/association_collection.rb:339:in `add_record_to_target_with_callbacks’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/associations/association_collection.rb:321:in `create_record’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/associations/association_collection.rb:172:in `create’
/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:196:in `write_audit’
/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:187:in `audit_update’
/vendor/plugins/acts_as_audited/lib/acts_as_audited/audit_sweeper.rb:52:in `before_update’
C:/jruby-1.1.6/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/observer.rb:171:in `update’
C:/jruby-1.1.6/lib/ruby/1.8/observer.rb:185:in `notify_observers’
C:/jruby-1.1.6/lib/ruby/1.8/observer.rb:184:in `each’
C:/jruby-1.1.6/lib/ruby/1.8/observer.rb:184:in `notify_observers’
jeet:
It looks like a bug in jRuby’s data serialization. It works fine for me on MRI.
Hey Brandon, awesome stuff thanks for creating this!
I assume it’s possible to do some pretty “diffs” between versions with strikeouts and green backgrounds for new text? Sort of like this: http://stackoverflow.com/revisions/14593/list
I’m going to be working on that so I will post a sample once I get it, but if someone already has example code for that I’d love to see it.
Thanks for a great plugin!
Brian
Just wanted to let everyone know I got the above working with pretty diff output. I used the HTMLDiff plugin here: http://github.com/myobie/htmldiff/tree/master
You can see the exact steps I took here:
http://stackoverflow.com/questions/80091/diff-a-ruby-string-or-array/739161#739161
Pretty neat stuff!!
Brian: Thanks for sharing. That is awesome!
Post a Comment