Sorry this page looks weird. It was automatically migrated from my old blog, which had a different layout and different CSS.

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

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-papertrail', :lib => 'papertrail', :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 © 2009 Andy Stewart (boss@airbladesoftware.com).
Released under the MIT licence.

Comments

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

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

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

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

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

Thanks for those links Alno, they look really useful.

Andy Stewart • 26 June 2009

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

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

Hi,

Great plugin! Have you thought about adding a diff method so you can get the differences between 2 different versions? Maybe including something like “http://github.com/tim/activerecord-diff” into the plugin would be nice.

Anyways, thanks, keep up the great work!

Craig

Craig • 4 June 2010

Hi Craig, I’ve thought about it but haven’t got round to doing it yet. I think I’ll need to do it fairly soon for a client though.

Thanks for the link to activerecord-diff. I wasn’t aware of it before.

Glad you like the code so far!

Andy Stewart • 4 June 2010

I don’t suppose you ever either made any progress on the storing of relationships, or know of a DRY way to do it?

Also, I looked for an API for Paper Trail, but couldn’t find one. Is there any way to drop all revisions older than a certain date, so that the database doesn’t get filled up with revisions that are clearly no longer useful?

William Jones • 22 June 2010

Hi William, I haven’t found a definitive way to store relationships. However I’ve been toying with the idea of “auto-reification”, i.e. when you reify or restore a particular version of an object, it automatically uses the versions of its related objects that were in play at that time. For this to work all those related objects would also have to be using PaperTrail. I hope to try this out fairly soon and will report back on my blog.

So far I haven’t added a method or rake task to drop all revisions older than a certain date because I (perhaps idly) thought people could take care of that themselves. However it would be useful and again I plan to do this pretty soon.

Andy Stewart • 22 June 2010

So each version stores the full information for that version (i.e., they aren’t storing diffs)? So I could just safely drop all versions older than a certain date with a cron job and nothing would be fouled up right?

William Jones • 22 June 2010

That’s exactly right. It’s what I do.

Andy Stewart • 22 June 2010

Craig: I’ve added to the README some info on diffing.

William: I’ve added to the README some info on deleting old versions.

Andy Stewart • 23 June 2010

Phenomenal, and thanks for the responsiveness. I will definitely check this out, looks perfect for a project I’m working on.

What would happen with Paper Trail if I were to add or remove a column from a model being tracked? Presumably everything would work fine from that point forward, but would all saved versions run into difficulties?

It seems like the correct response would be ignoring columns that have been removed from the model, and leaving default columns that the old version doesn’t know anything about.

William Jones • 25 June 2010

I have a question on how Paper Trail is meant to be used.

It has built in functionality to store the user responsible for the change. However, if I’m understanding Paper Trail correctly, each version is a snapshot of what the model looked like before the update. Therefore, the user stored along with each version is actually the user that made the model STOP looking like what’s stored in the version, rather than the user who made it look like what’s stored in the version.

Therefore, in order to display the ordinary usage case of a version along with the user that wrote its contents, I would actually have to pull the version previous to the version whose contents I’m displaying, and display the user from that one. Is that right?

William Jones • 26 June 2010

Yes, that’s how PaperTrail handles added/removed columns and how you would use it.

Also, when it encounters a column that no longer exists it’ll log a warning for you. See version.rb for details.

Identifying the user does sound convoluted when you write it out like that. I’ll simplify this, most probably with a method on version. Watch this space.

Thanks for the feedback!

Andy Stewart • 26 June 2010

William, this commit should help you out.

Andy Stewart • 28 June 2010

You, sir, are a scholar and a gentleman.

William Jones • 29 June 2010

Andrew Stewart • 23 June 2009 • RailsPaperTrail
You can reach me by email or on Twitter.