Thursday, August 6, 2009

Using acts_as_auditable for keeping history for model activities in rails

Keeping change history for important models are crucial for some application. And acts_as_auditable provides a clean implementation for keeping history.

Install

1. Download acts_as_auditable and put in your vendor folder

2. run

ruby script\generate audit

from your application directory. Which will generate a migration script to create audits table and a model named Audit.

Expectation

1. User model that inherits from ActiveRecord::Base with an instance method of auditor_name(it is required as it stores the auditors name as well as the auditor’s reference for keeping history).

2. A property named auditor(which will be injected in the model where you are using audit) should be prefilled with the user object who is changing the model.

3. Now you can use audit in your model as,

audit :when => :before_update,
      :if => lambda {|work_item| work_item.changed? },
      :with_message => lambda { |post| 'Something changed :)' }

Here

:when         => any active record callbacks (like :before_create)
:if           => provide a condition which will be checked to create the audit
:with_message => the message to be stored

Well that’s about it to use acts_as_auditable.

Implementing in real project

The challenge that I faced is to create a human readable message for model and make it maintainable.

I actually used audit like,

audit :when => :before_update,
      :if => lambda {|work_item| work_item.changed? },
      :with_message => lambda { |work_item| work_item.audit_message }

Now here is the code to generate the message.

def audit_message
  history_text = ''
  self.changes.each do |field, change|
    history_text += "#{field.humanize} changed from '#{change[0]}' to '#{change[1]}'\n" unless field.ends_with?('_id')
  end

  history_text += audit_message_for('project_id', Project, :name, 'project')
  history_text += audit_message_for('sprint_id', Sprint, :name, 'sprint')
  history_text += audit_message_for('responsible_person_id', User, :full_name, 'responsible_person', 'Responsible Person')
  history_text += audit_message_for('release_id', Release, :name, 'release')
  history_text += audit_message_for('point_id', Point, :name, 'point')

  return history_text
end

And here is the nasty part that at least cleans the audit message implementation. It helps to generate audit message for reference objects.

private
# Used for making the audit text readable
#
# ==== Parameters
#
# * +field+ - The database field to inspect changes
# * +klass+ - The class for the referenced object
# * +klass_property+ - The property of the object which will be readable to store audit data
# * +readable_name+ - readable name for the class. By default it is klass.to_s
#
# ==== Returns
# The audit text for that field.
# If there is no change in the field then empty string is returned
def audit_message_for(field, klass, klass_property, belongs_to, readable_name = klass.to_s)
  history_text = ''
  seperator = '\n'
  #as this is done with reflaction, catching exeptions for protection
  begin
    if self.send("#{field}_changed?")
      field_was = "#{field}_was"

      changed_from = klass.find(self.send(field_was)).send(klass_property) unless self.send(field_was) == nil
      changed_to   = self.send(belongs_to).send(klass_property) unless self.send(field) == nil

      if self.send(field_was) == nil
        history_text = "#{readable_name} assigned to '#{changed_to}'#{seperator}"
      elsif self.send(field) == nil
        history_text = "Removed #{readable_name} from '#{changed_from}'#{seperator}"
      else
        history_text = "#{readable_name} changed from '#{changed_from}' to '#{changed_to}'#{seperator}"
      end
    end
  rescue Exception => ex
    logger.error "Error #{ex.to_s}"
  end

  return history_text
end

Another thing to consider is assigning the auditor before saving the history. It is cleaner if you define a method named auditor. That is how you can avoid assigning to that attribute.

def auditor
  return self.new_record? ? self.creator : self.updator
end

But for implementing like this you need to modify some code in the library. Open the vendor\plugins\acts_as_auditable\lib\shooter\acts\auditable.rb and remove the line,

attr_accessor :auditor

Otherwise your method will be overwritten.

No comments: