views:

558

answers:

4

We recently began a compliance push at our company and are required to keep a full history of changes to our data which is currently managed in a Rails application. We've been given the OK to simply push something descriptive for every action to a log file, which is a fairly unobtrusive way to go.

My inclination is to do something like this in ApplicationController:

around_filter :set_logger_username

def set_logger_username
  Thread.current["username"] = current_user.login || "guest"
  yield
  Thread.current["username"] = nil
end

Then create an observer that looks something like this:

class AuditObserver < ActiveRecord::Observer
  observe ... #all models that need to be observed

  def after_create(auditable)
    AUDIT_LOG.info "[#{username}][ADD][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
  end

  def before_update(auditable)
    AUDIT_LOG.info "[#{username}][MOD][#{auditable.class.name}][#{auditable.id}]:#{auditable.changed.inspect}"
  end

  def before_destroy(auditable)
    AUDIT_LOG.info "[#{username}][DEL][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
  end

  def username
    (Thread.current['username'] || "UNKNOWN").ljust(30)
  end
end

and in general this works great, but it fails when using the "magic" <association>_ids method that is tacked to has_many :through => associations.

For instance:

# model
class MyModel
  has_many :runway_models, :dependent => :destroy
  has_many :runways, :through => :runway_models
end

#controller
class MyModelController < ApplicationController

  # ...

  # params => {:my_model => {:runways_ids => ['1', '2', '3', '5', '8']}}

  def update
    respond_to do |format|
      if @my_model.update_attributes(params[:my_model])
        flash[:notice] = 'My Model was successfully updated.'
        format.html { redirect_to(@my_model) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @my_model.errors, :status => :unprocessable_entity }
      end
    end
  end

  # ...
end

This will end up triggering the after_create when new Runway records are associated, but will not trigger the before_destroy when a RunwayModel is deleted.

My question is... Is there a way to make it work so that it will observe those changes (and/or potentially other deletes)?
Is there a better solution that is still relatively unobtrusive?

A: 

You could also use something like acts_as_versioned http://github.com/technoweenie/acts_as_versioned
It versions your table records and creates a copy every time something changes (like in a wiki for instance)
This would be easier to audit (show diffs in an interface etc) than a log file

clyfe
+1  A: 

Hi,

I had a similar requirement on a recent project. I ended using the acts_as_audited gem, and it worked great for us.

In my application controller I have line like the following

audit RunWay,RunWayModel,OtherModelName

and it takes care of all the magic, it also keeps a log of all the changes that were made and who made them-- its pretty slick.

Hope it helps

Jonathan
+1  A: 

Added this monkey-patch to our lib/core_extensions.rb

ActiveRecord::Associations::HasManyThroughAssociation.class_eval do 
  def delete_records(records)
    klass = @reflection.through_reflection.klass
    records.each do |associate|
      klass.destroy_all(construct_join_attributes(associate))
    end
  end
end

It is a performance hit(!), but satisfies the requirement and considering the fact that this destroy_all doesn't get called often, it works for our needs--though I am going to check out acts_as_versioned and acts_as_audited

Mark S.
I have my doubts about whether the acts_as_versioned and acts_as_audited handle has_many through associations. But, this works well, and is not much of a performance hit. Just to be sure, I made this change in the ActiveRecord lib and ran its unit tests. Everything passed with the exception of three tests that check the number of SQL queries that get run. In those three cases, there was one extra "select" statement (selecting by the association Ids), and the "delete" statement now selects by the join table's PK instead. Note that if you're also using HABTM relations, you may need another patch
Chris
A: 

Use the Vestal versions plugin for this:

Refer to this screen cast for more details. Look at the similar question answered here recently.

Vestal versions plugin is the most active plugin and it only stores delta. The delta belonging to different models are stored in one table.

class User < ActiveRecord::Base
  versioned
end

# following lines of code is from the readme    
>> u = User.create(:first_name => "Steve", :last_name => "Richert")
=> #<User first_name: "Steve", last_name: "Richert">
>> u.version
=> 1
>> u.update_attribute(:first_name, "Stephen")
=> true
>> u.name
=> "Stephen Richert"
>> u.version
=> 2
>> u.revert_to(10.seconds.ago)
=> 1
>> u.name
=> "Steve Richert"
>> u.version
=> 1
>> u.save
=> true
>> u.version
=> 3
KandadaBoggu