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!
Comments on "Keep a Paper Trail with Paper Trail"
Write more, thats all I have to say. Literally, it seems as though you relied on the video to make your point. You obviously know what youre talking about, why waste your intelligence on just posting videos to your blog when you could be giving us something enlightening to read?
Would you be fascinated about exchanging links?
I see something really special in this web site.
Thanks for every other great post. Where else could anybody get that type of information in such a perfect means of writing? I’ve a presentation subsequent week, and I am on the search for such info.
YGXF68 Some genuinely prize blog posts on this site, saved to bookmarks.
Life can be tough at times, you put in the hours of work and only end up with blank stares. You know what I mean? That’s the time posts like this keep me motivated.
Thanks for another ginerity excellent article. Where else could anybody get that type of info in such a perfect way of writing? I’ve a presentation next week, and I am on the look for such info.
Wonderful story, reckoned we could combine a handful of unrelated data, nevertheless seriously worth taking a appear, whoa did 1 understand about Mid East has got more problerms as well.
Usually posts some incredibly intriguing stuff like this. If you?re new to this site.
Thanks-a-mundo for the blog.Really thank you! Keep writing.
Very good blog article.Thanks Again. Great.
I like this post, enjoyed this one thank you for putting up. “I never let schooling interfere with my education.” by Mark Twain.
Great post.
Hello baby
That article is very helpful for me,i like it,thanks!
hermes belt buy http://bionor.es/cas/pdf/77/19/
Although internet websites we backlink to beneath are considerably not connected to ours, we really feel they may be in fact really worth a go by way of, so possess a look.
Here are a few of the internet sites we suggest for our visitors.
Just beneath, are numerous entirely not associated web pages to ours, having said that, they’re surely really worth going over.
It’s an remarkable post in support of each of the online
people; they are going to get benefit from using it I am certain.
my weblog – PerryDLangel
FbWmSV koxjxpxqnpbs, [url=http://ppfhqzmcrimh.com/]ppfhqzmcrimh[/url], [link=http://drfuopqgojpf.com/]drfuopqgojpf[/link], http://ohwheplbpwxa.com/
Usually posts some extremely intriguing stuff like this. If you?re new to this site.
Here are several of the web-sites we advise for our visitors.
Just beneath, are many absolutely not connected web-sites to ours, on the other hand, they may be surely worth going over.
This excellent website certainly has all of the information I needed
concerning this subject and didn’t know who to ask.
My site … RyanADeist
great issues altogether, you simply won a logo new reader.
What would you suggest about your post that you simply made some days before?
Any positive?
my site – MicahJToolan