The Limitation of PaperTrail

PaperTrail is fantastic, but in every app I’ve worked on, there’s at least two classes of users: Admin and User. Now, admins can change things and in some applications, users can, too. In business environments, different user classes may be able to edit your models. Any financial clients or ones with business sensitive information will want to keep an audit trail to see who’s updating what across the app. Out of the box, PaperTrail doesn’t support keeping track of multiple types of users. Good for us that PaperTrail’s provided us APIs to alter its behavior.

Intro to Whodunnit

By default, PaperTrail allows you to track which user edited a certain object with a few lines of configuration.

All you need is a current_user method for a given controller, and then to add the following:

class ApplicationController
  before_filter :set_paper_trail_whodunnit
end

Note, ApplicationController can also be AdminController or whatever other top level controller all editing behavior is done under. So long as all other controllers that handle Create, Update, Delete functionality inherit from this controller or have their own before_filter :set_paper_trail_whodunnit, you’re good to go.

Going forward, this post assumes you’ve got this filter set correctly.

What It Does

class PaperTrail::Version < ActiveRecord::Base {
  :created_at     => :datetime,
  :event          => :string,
  :id             => :integer,
  :item_id        => :integer,
  :item_type      => :string,
  :object         => :text,
  :object_changes => :text,
  :whodunnit      => :string,
}

Looking at the above, you’ll see the :whodunnit field. Even though it’s a string, in the default configuration it stores an integer, which is the id of the editing person. This is set when an object is editing through a controller action.

Extending to Polymorphic Behavior

What needs to happen to get this to work with multiple user classes? If you haven’t yet, think about what happens when Admin.find(1) and Lackey.find(1) both edit the same object. :whodunnit is set to "1" in both cases. That’s correct, but is only half of what’s needed.

Migration

Rails standard convention for polymorphism is to use *_type as the field name to specify which class to look up. In our case, just create the following migration and you’re good to go:

rails g migration add_whodunnit_type to versions whodunnit_type:string

The migration should have the following:

def change
  add_column :versions, :whodunnit_type, :string
end

rake db:migrate, and we’re ready for the next step.

Controller Changes

PaperTrail provides a helpful method for what we need: info_for_PaperTrail. Use this within the controller to pass extra info to the creation of a PaperTrail::Version object. Add this:

class ApplicationController < ActionController::Base
  def info_for_paper_trail
    {whodunnit_type: @current_user.class.name}
  end
end

This passes in a new attribute that will be saved on each Version with what’s needed to lookup the specific user that did it in a polymorphic way.

Adding to PaperTrail

The final part to make this developer friendly is to add an easy way to load up the responsible party. Ideally, Version#whodunnit would be Version.whodunnit_id. Since it’s not, I defined Version#perpetrator.

#initializers/paper_trail.rb
PaperTrail::Version.class_eval do
  def perpetrator
    whodunnit_type.constantize.find(whodunnit.to_i) if (!!whodunnit && !!whodunnit_type)
  end
end

This will take the class name, constantize it, and then return the perpetrator. Quick, easy and does what’s needed to get you up and running with polymorphic whodunnit on PaperTrail.

Wrapping Up

PaperTrail’s versioning and whodunnit to track auditing is fantastic. However, it falls apart if you have multiple user classes, such as Admin and User. Read on to keep your auditing trail clean and exact. With the above changes you can now track everyone across your app and when clients or users need to see changes, you can respond and display with 100% confidence the correct perpetrator of a given update on your models.