opensoul.org

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.

  1. 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.

acts_as_audited, plugin, rails, and ruby June 18, 2007

25 Comments

  1. Alexander Flatter Alexander Flatter June 18, 2007

    That’s funny.. I just looked over your old blog post and thought I could use this… :-)
    Thanks for your work.

  2. carlivar carlivar June 18, 2007

    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.

  3. carlivar carlivar July 26, 2007

    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… :)

  4. shadoi shadoi July 30, 2007

    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?

  5. shadoi shadoi July 30, 2007

    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’

  6. Irfan Baig Irfan Baig August 9, 2007

    Hey this is cool! =)

  7. Geoffrey Wiseman Geoffrey Wiseman August 23, 2007

    Would be nice if you could retrieve a revision by date as well, since the audit table holds a created_at.

  8. Geoffrey Wiseman Geoffrey Wiseman August 23, 2007

    Would also love this to co-operate better with Globalize model translations.

  9. Tim Chater Tim Chater September 18, 2007

    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.

  10. Mantat Mantat October 7, 2007

    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?

  11. Ryan Ryan October 10, 2007

    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?

  12. Brandon Brandon October 12, 2007

    Ryan,

    I’ll try to clean up the one that I have and post it. Stay tuned.

  13. Surat Mukker Surat Mukker October 15, 2007

    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

  14. Ryan Ryan December 6, 2007

    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?

  15. Brandon Brandon December 6, 2007

    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.

  16. AH AH December 21, 2007

    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.

  17. Aaron Aaron January 31, 2008

    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)).

  18. Daniel Schierbeck Daniel Schierbeck June 29, 2008

    I’m getting the following error when calling the

    revisions
    method on an audited object.

    NoMethodError: undefined method `first' for Sun Jun 29 17:54:32 +0200 2008:Time
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:149:in `changes'
    	from (irb):5:in `inject'
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:148:in `each'
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:148:in `inject'
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:148:in `changes'
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:147:in `collect'
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:147:in `changes'
    	from /home/daniel/Projects/mango/vendor/plugins/acts_as_audited/lib/acts_as_audited.rb:133:in `revision'
    	from (irb):5

    Am I the only one with this problem?

  19. Nolan Eakins Nolan Eakins July 15, 2008

    I’m surprised that acts_as_audited was your plugin. It’ll probably make a guest appearance here at Bluefish.

  20. Irfan Irfan November 11, 2008

    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

  21. jeet jeet February 21, 2009

    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’

  22. Brandon Brandon February 23, 2009

    jeet:

    It looks like a bug in jRuby’s data serialization. It works fine for me on MRI.

  23. Brian Armstrong Brian Armstrong April 10, 2009

    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

  24. Brian Armstrong Brian Armstrong April 10, 2009

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

  25. Brandon Brandon April 15, 2009

    Brian: Thanks for sharing. That is awesome!

Post a Comment

Comments use textile. Anonymous comments will be deleted.

My name is Brandon Keepers. I like to build things, usually in Ruby or JavaScript. I work at GitHub and live in Holland, MI.

Popular Posts