Keep a Paper Trail with Paper Trail

Who did what when and to which? Don't guess. Track changes with this Rails add-on.

Keeping track of who did what and when is a common problem in any significant Web application. A canonical instance is found in wikis, where every piece of content is editable. Wiki software records all activity to provide for review and oversight and to provide recourse should errors be introduced in the system. (Indeed, that very auditing of additions and revisions encourages contribution.) However, auditing is just as important (or more so) in traditional editorial workflows, and no less essential in inventory management, where modifications can affect financial performance. Ultimately, any system, whether it has one user or one thousand users, can benefit from auditing. After all, humans make clerical errors, introduce bugs in code, and invent Ponzi schemes.

In general, coders should audit transactions whenever possible. While a regular backup plan hedges against data loss, audits safeguard against information loss. It’s fairly easy to cook up a solution—say, hook the CRUD functions in your application—but if you’re a Rails developer, there are a good number of capable gems and plug-ins ready for use. Ryan Bates looked at vestal versions in a recent Railscast, and acts_as_versioned has been available for quite some time. Here, I want to look at another impressive solution called, aptly, Paper Trail.

Paper Trail, as its name implies, creates a running record of changes in each ActiveRecord models you augment. Moreover, if you provide a current_user method in your ApplicationController, Paper Trail also journals who made each change. You can enumerate all the modifications made to a model, revert to a prior revision of a model, and even undelete a model. Smartly, Paper Trail can also be enabled and disabled programatically during migrations. Better yet, all these features are inherited with one addition to your code:

class Widget < ActiveRecord::Base
  has_paper_trail
end

Installing Paper Trail

Let’s install Paper Trail and see how it works.

Paper Trail is available as both a plug-in and a gem. Let’s use the latter form so all Rails applications can benefit. To begin, create a new Rails application, edit the file config/environment.rb, and add a new config.gem entry to reflect the dependency on the Paper Trial gem.

$ cd /tmp
$ rails myapp
$ cd myapp
$ vi config/environment.rb
RAILS_GEM_VERSION = '2.3.4' unless defined? RAILS_GEM_VERSION
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  …
  config.gem 'airblade-paper_trail', :lib => 'paper_trail', :source => 'http://gems.github.com'
  …
end

After you edit the file, install the gem via rake.

$ sudo rake gems:install
gem install airblade-paper_trail --source http://gems.github.com
Successfully installed airblade-paper_trail-1.1.1
1 gem installed
Installing ri documentation for airblade-paper_trail-1.1.1...
Installing RDoc documentation for airblade-paper_trail-1.1.1...

Paper Trail maintains model versions its own set of tables, so the next is to create a suitable migration file and alter the database.

$ ruby ./script/generate paper_trail
$ rake db:migrate
==  CreateVersions: migrating
-- create_table(:versions)
   -> 0.0027s
-- add_index(:versions, [:item_type, :item_id])
   -> 0.0005s
==  CreateVersions: migrated (0.0037s)

At this point, you are ready to go. Don’t forget to add has_paper_trial to models you want to audit.

Shuffling Papers

Let’s see how Paper Trial works. To jumpstart the new, empty application, use a scaffold to create a new model, view, controller and migration. Call the new class a Gadget and assign it a handful of fields.

$ ruby ./script/generate scaffold gadget \
  name:string description:string qty:integer
$ rake db:migrate
==  CreateGadgets: migrating
-- create_table(:gadgets)
   -> 0.0023s
==  CreateGadgets: migrated (0.0024s)

Open the file app/models/gadget.rb and add has_paper_trail.

class Gadget < ActiveRecord::Base
  has_paper_trail
end

Now fire up the server, point your browser to http://localhost:3000/gadgets/, create a record or two and make revisions.

$ ruby ./script/server
=> Booting Mongrel
=> Rails 2.3.4 application starting on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server

(If you are running Rails 2.3.3, upgrade as soon as possible to version 2.3.4 to avoid security flaws.) To view the revisions, drop into the console.

$ ./script/console
>> g = Gadget.first
=> #<Gadget id: 1, name: "Loaferberry",
description: "All-in-one footware and phone book",
qty: 200, created_at: "2009-09-16 12:16:34",
updated_at: "2009-09-16 12:21:14">

>> g.versions.size
4

>> g.versions.first
=> #<Version id: 1, item_type: "Gadget",
item_id: 1, event: "create", whodunnit: nil,
object: nil, created_at: "2009-09-16 12:16:34">

>> g.versions[1]
=> #<Version id: 2, item_type: "Gadget",
item_id: 1, event: "update", whodunnit: nil,
object: "--- \nqty: 10\nname: Shoe telephone\nupdated_at: 2009-...",
created_at: "2009-09-16 12:16:59">

>> g.name
=> "Loaferberry"

>> g.versions[-2].reify
=> #<Gadget id: 1, name: "Shoe telephone",
description: "All-in-one sneaker and speaker, and now speed diale...",
qty: 20, created_at: "2009-09-16 12:16:34",
updated_at: "2009-09-16 12:16:59">

>> g = g.versions[-2].reify
=> #<Gadget id: 1, name: "Shoe telephone",
description: "All-in-one sneaker and speaker, and now speed diale...",
qty: 20, created_at: "2009-09-16 12:16:34",
updated_at: "2009-09-16 12:16:59">

>> g.save
=> true

>> g.reload
=> #<Gadget id: 1, name: "Shoe telephone",
description: "All-in-one sneaker and speaker, and now speed diale...",
qty: 20, created_at: "2009-09-16 12:16:34",
updated_at: "2009-09-16 12:37:45">

>> g.versions.size
=> 5

>> g.versions[-1]
=> #<Version id: 5, item_type: "Gadget", item_id: 1,
event: "update", whodunnit: nil,
object: "--- \nqty: 200\nname: Loaferberry\nupdated_at: 2009-09...",
created_at: "2009-09-16 12:37:45">

has_paper_trail adds a one-to-many relationship between a model and its versions. Hence, Gadget.find(1).versions yields an array of revisions.

Each revision stores the state of the object before a change was applied; the current state of an object is simply the model itself.

Thus, Gadget.find(1).versions[-1], always refers to the previous revision of the model with ID 1. The first version, ….versions.first, is special: it records the creation of the model. Its prior version is nil, since the model is newly created.

Paper Trail uses the field object to record a version of a model in JSON-like format. To convert from that internal representation back to ActiveRecord, use the method reify.

You can also reconstitute a destroyed object.

>> Gadget.find(1).destroy

>> Gadget.find(1)
ActiveRecord::RecordNotFound: Couldn't find Gadget with ID=1

>> Version.find_all_by_item_id(1).last
=> #<Version id: 6, item_type: "Gadget", item_id: 1,
event: "destroy", whodunnit: nil,
object: "--- \nqty: 20\nname: Shoe telephone\nupdated_at: 2009-...",
created_at: "2009-09-16 13:00:34">

By design, Paper Trail does not store a duplicate of the current object, just the most penultimate revision. So, when you restore an object that’s been deleted, it returns to the state before it was destroyed. This makes some sense, too. If the current object was errant and was deleted as a result, you want to restore to the previous good state.

Not Just for Bean Counters

An audit trail, as Paper Trail produces, is invaluable. I did not show its use here, but Paper Trail records who made each change, too,in the whodunnit field. Given the who and what, you can deduce most any problem and have evidence of the change. You should consider vestal versions and other audit packages, too. The former lets you rollback to a specific revision and to a specific date. Very cool.

Happy tinkering!

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62