A Paper Trail For Your Models

PaperTrail lets you track changes to your Rails app’s models’ data. It’s good for auditing or versioning. You can see how a model looked at any stage in its lifecycle and even undelete it after it’s been destroyed.

Features

  • Stores every create, update and destroy.
  • Does not store updates which don’t change anything.
  • 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.
  • Automatically records who was responsible if your controller has a current_user method.
  • Allows you to set who is responsible at model-level (useful for migrations).
  • Can be turned off/on (useful for migrations).
  • No configuration necessary.
  • Stores everything in a single database table (generates migration for you).
  • Thoroughly tested.

Rails Version

Known to work on Rails 2.3. Probably works on Rails 2.2 and 2.1.

Basic Usage

PaperTrail is simple to use. Just add 15 characters to a model to get a paper trail of every create, update, and destroy.


class Widget < ActiveRecord::Base
  has_paper_trail
end

This gives you a versions method which returns the paper trail of changes to your model.


>> widget = Widget.find 42
>> widget.versions             # [<Version>, <Version>, ...]

Once you have a version, you can find out what happened:


>> v = widget.versions.last
>> v.event                     # 'update' (or 'create' or 'destroy')
>> v.whodunnit                 # '153'  (if the update was via a controller and
                               #         the controller has a current_user method,
                               #         here returning the id of the current user)
>> v.created_at                # when the update occurred
>> widget = v.reify            # the widget as it was before the update;
                               # would be nil for a create event

PaperTrail stores the pre-change version of the model, unlike some other auditing/versioning plugins, so you can retrieve the original version. This is useful when you start keeping a paper trail for models that already have records in the database.


>> widget = Widget.find 153
>> widget.name                                 # 'Doobly'

# Add has_paper_trail to Widget model.

>> widget.versions                             # []
>> widget.update_attributes :name => 'Wotsit'
>> widget.versions.first.reify.name            # 'Doobly'
>> widget.versions.first.event                 # 'update'

This also means that PaperTrail does not waste space storing a version of the object as it currently stands. The versions method gives you previous versions; to get the current one just call a finder on your Widget model as usual.

Here’s a helpful table showing what PaperTrail stores:

Event Model Before Model After
create nil widget
update widget widget’
destroy widget nil

PaperTrail stores the values in the Model Before column. Most other auditing/versioning plugins store the After column.

Reverting Or Undeleting A Model

PaperTrail makes reverting to a previous version easy:


>> widget = Widget.find 42
>> widget.update_attributes :name => 'Blah blah'
# Time passes....
>> widget = Widget.find(42).versions.last.reify  # the widget as it was before the update
>> widget.save  # reverted to its previous version

Undeleting is just as simple:


>> widget = Widget.find 42
>> widget.destroy
# Time passes....
>> widget = Version.find(153).reify    # the widget as it was before it was destroyed
>> widget.save                         # the widget lives!

In fact you could use PaperTrail to implement an undo system, though I haven’t had the opportunity yet to do it myself.

Finding Out Who Was Responsible For A Change

If your ApplicationController has a current_user method, PaperTrail will store the value it returns in the version’s whodunnit column. Note that this column is a string so you will have to convert it to an integer if it’s an id and you want to look up the user later on:


>> last_change = Widget.versions.last
>> user_who_made_the_change = User.find last_change.whodunnit.to_i

In a migration or in script/console you can set who is responsible like this:


>> PaperTrail.whodunnit = 'Andy Stewart'
>> widget.update_attributes :name => 'Wibble'
>> widget.versions.last.whodunnit              # Andy Stewart

Turning PaperTrail Off/On

Sometimes you don’t want to store changes. Perhaps you are only interested in changes made by your users and don’t need to store changes you make yourself in, say, a migration.

If you are about change some widgets and you don’t want a paper trail of your changes, you can turn PaperTrail off like this:


>> Widget.paper_trail_off

And on again like this:


>> Widget.paper_trail_on

Installation

  1. Install PaperTrail either as a gem or as a plugin:

    config.gem 'airblade-paper_trail', :lib => 'paper_trail', :source => 'http://gems.github.com'

    or:

    script/plugin install git://github.com/airblade/paper_trail.git

  2. Generate a migration which will add a versions table to your database.

    script/generate paper_trail

  3. Run the migration.

    rake db:migrate

  4. Add has_paper_trail to the models you want to track.

Testing

PaperTrail has a thorough suite of tests. However they only run when PaperTrail is sitting in a Rails app’s vendor/plugins directory. If anyone can tell me how to get them to run outside of a Rails app, I’d love to hear it.

Problems

Please use GitHub’s issue tracker.

Inspirations

Intellectual Property

Copyright (c) 2009 Andy Stewart (boss@airbladesoftware.com). Released under the MIT licence.

Andy Stewart, 23 June 2009

Posted in PaperTrail, Rails


  1. What will happen when a parent ( who has a paper trail) has_many children ? If i update the children will this create a new version of the parent?

    Ryan R. Smith
    24 June 2009
  2. Ryan, updating the children won't create a new version of the parent because the parent hasn't changed.

    Conversely updating the parent will create a new version for the parent but it won't cascade down and create new versions for all the children too.

    Andy Stewart
    25 June 2009
  3. Awesome work Andy.

    On the aprent/child conversation, it seems that if a relationships changes on the parent (like adding a new child), that is not going to be tracked as a version of the parent.

    How would you suggest an approach to tackle that issue? Asking this because sometimes what defines a version are actually the related information that it own version.

    Luis Lavena
    25 June 2009
  4. Thanks Luis.

    When PaperTrail creates a new version of an object, it stores the object's attributes but not its relationships. Initially I wrote some code to store an object's relationships too, but threw it away when I realised I didn't need it.

    However I got far enough to realise I was implementing deep cloning, which Jan De Poorter has already done in his Deep Cloning plugin.

    So I would suggest approaching versioning based on changes like adding a new child by trying to integrate Jan's plugin.

    In the meantime, if you simply want to record when a child is added or removed from a parent, you could set up a counter cache on the parent. That's simply an attribute so PaperTrail would create a new version of the parent whenever it changed. However if you reified the pre-change version of the parent, its counter cache would be wrong because it would have the post-change children associated with it.

    Andy Stewart
    25 June 2009
  5. About testing plugin outside rails application:

    In your case it should be enough to create test models and establish connection to test db. You can find some information about it in this article.

    Also, there are gems, which help to test rails plugins outside of application, such as dry_plugin_test_helper and plugin_test_helper

    Alno
    26 June 2009
  6. Thanks for those links Alno, they look really useful.

    Andy Stewart
    26 June 2009
  7. Hey Andy,

    I love your plugin, but I am really needing to be able to store a models relationships, so that I do not have to pick the different models to fetch audits from each time. It does not seem very DRY at the moment (maybe I am not implementing correctly)

    Let me give you an example to explain. Lets say I have an account model, obviously I want to know who changes account data, but I also want to see associated changes in a models relationships like, comments, files, tasks etc.

    The idea is i call Account.versions just the one time (and implemented just the one time), and I get an audit of who has done what on an account. This way at least means I implement only once and any new relations would be automatically added?

    At the moment I have to pick the models I want with a select query ie. Version.find(:all, :conditions => "item_type IN (#{related_models.join("','").wrap("'")})", :limit => 25)

    Any help on the subject would be very much appreciated?

    Carl
    16 November 2009
  8. Hi Carl,

    You're right: there isn't a nice DRY way currently to do what you want to do. PaperTrail is geared more towards versioning than auditing, and I haven't yet found a way to store relationships.

    It would be straightforward, I think, to make the has_paper_trail declaration cascade out to related models automatically. And PaperTrail could then query the versions table for items of a type related to your Account class. But I can't see a scalable way to ensure the returned versions are related to your particular Account instance. (Your query must return items related to any and all accounts, not just a particular one?) We could of course add extra columns to the versions table, e.g. {parent_type, parent_id}, like a polymorphic foreign key...though I'm not sure whether that would cover all eventualities.

    I'd like to figure this out because I have a similar situation to you in one of my applications. However I'm maxed out on other things at the moment...feel free to fork the code and send me pull requests ;)

    Andy Stewart
    16 November 2009

Have your say

You can use Markdown in your comments. If you want to post code, do this:

<pre><code class="ruby|javascript|css|html">your code here</code></pre>

Thanks!