require 'set'
# Audit saves the changes to ActiveRecord models. It has the following attributes:
#
# * auditable: the ActiveRecord model that was changed
# * user: the user that performed the change; a string or an ActiveRecord model
# * action: one of create, update, or delete
# * changes: a serialized hash of all the changes
# * created_at: Time that the change was performed
#
class Audit < ActiveRecord::Base
belongs_to :auditable, :polymorphic => true
# belongs_to :user, :polymorphic => true
# belongs_to :on_behalf_of, :class_name => 'User', :foreign_key => 'on_behalf_of_id'
before_create :set_version_number, :set_audit_user
serialize :changes
cattr_accessor :audited_class_names
self.audited_class_names = Set.new
class << self
def audited_classes
self.audited_class_names.map(&:constantize)
end
# All audits made during the block called will be recorded as made
# by +user+. This method is hopefully threadsafe, making it ideal
# for background operations that require audit information.
def as_user(user, &block)
Thread.current[:acts_as_audited_user] = user
yield
Thread.current[:acts_as_audited_user] = nil
end
def manual_audit(user, action, on_behalf_of = nil, auditable = nil)
attribs = { :action => action }
case user
when ActiveRecord::Base
attribs[CollectiveIdea::Acts::Audited.human_model] = user
when String
attribs[:username] = user
end
case auditable
when ActiveRecord::Base
attribs[:auditable] = auditable
when String
attribs[:auditable_type] = auditable
end
attribs[:on_behalf_of] = on_behalf_of if on_behalf_of
Audit.create attribs
end
end
# Allows user to be set to either a string or an ActiveRecord object
def user_as_string=(user) #:nodoc:
# reset both either way
self.user_as_model = self.username = nil
user.is_a?(ActiveRecord::Base) ?
self.user_as_model = user :
self.username = user
end
# alias_method :user_as_model=, :user=
# alias_method :user=, :user_as_string=
def user_as_string #:nodoc:
self.user_as_model || self.username
end
# alias_method :user_as_model, :user
# alias_method :user, :user_as_string
def revision
clazz = auditable_type.constantize
returning clazz.find_by_id(auditable_id) || clazz.new do |m|
Audit.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge({:version => version}))
end
end
def ancestors
self.class.find(:all, :order => 'version',
:conditions => ['auditable_id = ? and auditable_type = ? and version <= ?',
auditable_id, auditable_type, version])
end
# Returns a hash of the changed attributes with the new values
def new_attributes
(changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
attrs[attr] = Array(values).last
attrs
end
end
# Returns a hash of the changed attributes with the old values
def old_attributes
(changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
attrs[attr] = Array(values).first
attrs
end
end
def self.reconstruct_attributes(audits)
attributes = {}
result = audits.collect do |audit|
attributes.merge!(audit.new_attributes).merge!(:version => audit.version)
yield attributes if block_given?
end
block_given? ? result : attributes
end
def self.assign_revision_attributes(record, attributes)
attributes.each do |attr, val|
if record.respond_to?("#{attr}=")
record.attributes.has_key?(attr.to_s) ?
record[attr] = val :
record.send("#{attr}=", val)
end
end
record
end
private
def set_version_number
max = self.class.maximum(:version,
:conditions => {
:auditable_id => auditable_id,
:auditable_type => auditable_type
}) || 0
self.version = max + 1
end
# def set_audit_user
# self.user = Thread.current[:acts_as_audited_user] if Thread.current[:acts_as_audited_user]
# nil # prevent stopping callback chains
# end
end