Skip to content

Commit

Permalink
Support for thread-safe 'background auditing'
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethkalmer authored and bkeepers committed Jun 3, 2009
1 parent 21fd3ac commit b624ba0
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 63 deletions.
6 changes: 6 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ To get auditing outside of Rails you can explicitly declare <tt>acts_as_audited<
acts_as_audited :except => [:password, :mistress]
end

To record a user in the audits when the sweepers are not available, you can use <tt>as_user</tt>:

Audit.as_user( user ) do
# Perform changes on audited models
end

See http://opensoul.org/2006/07/21/acts_as_audited for more information.

== Caveats
Expand Down
77 changes: 42 additions & 35 deletions lib/acts_as_audited.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Copyright (c) 2006 Brandon Keepers
#
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Expand Down Expand Up @@ -43,8 +43,8 @@ module ClassMethods
#
# * +only+ - Only audit the given attributes
# * +except+ - Excludes fields from being saved in the audit log.
# By default, acts_as_audited will audit all but these fields:
#
# By default, acts_as_audited will audit all but these fields:
#
# [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
# You can add to those by passing one or an array of fields to skip.
#
Expand All @@ -60,16 +60,16 @@ module ClassMethods
# acts_as_audited :protect => false
# attr_accessible :name
# end
#
#
def acts_as_audited(options = {})
# don't allow multiple calls
return if self.included_modules.include?(CollectiveIdea::Acts::Audited::InstanceMethods)

options = {:protect => accessible_attributes.nil?}.merge(options)

class_inheritable_reader :non_audited_columns
class_inheritable_reader :auditing_enabled

if options[:only]
except = self.column_names - options[:only].flatten.map(&:to_s)
else
Expand All @@ -81,27 +81,27 @@ def acts_as_audited(options = {})
has_many :audits, :as => :auditable, :order => "#{Audit.quoted_table_name}.version"
attr_protected :audit_ids if options[:protect]
Audit.audited_class_names << self.to_s

after_create :audit_create_callback
before_update :audit_update_callback
after_destroy :audit_destroy_callback

attr_accessor :version

extend CollectiveIdea::Acts::Audited::SingletonMethods
include CollectiveIdea::Acts::Audited::InstanceMethods

write_inheritable_attribute :auditing_enabled, true
end
end

module InstanceMethods

# Temporarily turns off auditing while saving.
def save_without_auditing
without_auditing { save }
end

# Executes the block with the auditing callbacks disabled.
#
# @foo.without_auditing do
Expand All @@ -111,7 +111,7 @@ def save_without_auditing
def without_auditing(&block)
self.class.without_auditing(&block)
end

# Gets an array of the revisions available
#
# user.revisions.each do |revision|
Expand All @@ -125,28 +125,28 @@ def revisions(from_version = 1)
revision = self.audits.find_by_version(from_version).revision
Audit.reconstruct_attributes(audits) {|attrs| revision.revision_with(attrs) }
end

# Get a specific revision specified by the version number, or +:previous+
def revision(version)
revision_with Audit.reconstruct_attributes(audits_to(version))
end

def revision_at(date_or_time)
audits = self.audits.find(:all, :conditions => ["created_at <= ?", date_or_time])
revision_with Audit.reconstruct_attributes(audits) unless audits.empty?
end

def audited_attributes
attributes.except(*non_audited_columns)
end

protected

def revision_with(attributes)
returning self.dup do |revision|
revision.send :instance_variable_set, '@attributes', self.attributes_before_type_cast
revision.attributes = attributes.reject {|attr,_| !respond_to?("#{attr}=") }

# Remove any association proxies so that they will be recreated
# and reference the correct object for this revision. The only way
# to determine if an instance variable is a proxy object is to
Expand All @@ -160,16 +160,16 @@ def revision_with(attributes)
end
end
end

private

def audited_changes
changed_attributes.except(*non_audited_columns).inject({}) do |changes,(attr, old_value)|
changes[attr] = [old_value, self[attr]]
changes
end
end

def audits_to(version = nil)
if version == :previous
version = if self.version
Expand All @@ -182,7 +182,7 @@ def audits_to(version = nil)
end
audits.find(:all, :conditions => ['version <= ?', version])
end

def audit_create(user = nil)
write_audit(:action => 'create', :changes => audited_attributes, :user => user)
end
Expand All @@ -196,20 +196,20 @@ def audit_update(user = nil)
def audit_destroy(user = nil)
write_audit(:action => 'destroy', :user => user, :changes => audited_attributes)
end

def write_audit(attrs)
self.audits.create attrs if auditing_enabled
end

CALLBACKS.each do |attr_name|
CALLBACKS.each do |attr_name|
alias_method "#{attr_name}_callback".to_sym, attr_name
end

def empty_callback #:nodoc:
end

end # InstanceMethods

module SingletonMethods
# Returns an array of columns that are audited. See non_audited_columns
def audited_columns
Expand All @@ -227,31 +227,38 @@ def without_auditing(&block)
disable_auditing
returning(block.call) { enable_auditing if auditing_was_enabled }
end

def disable_auditing
write_inheritable_attribute :auditing_enabled, false
end

def enable_auditing
write_inheritable_attribute :auditing_enabled, true
end

def disable_auditing_callbacks
class_eval do
CALLBACKS.each do |attr_name|
alias_method "#{attr_name}_callback", :empty_callback
end
end
end

def enable_auditing_callbacks
class_eval do
class_eval do
CALLBACKS.each do |attr_name|
alias_method "#{attr_name}_callback".to_sym, attr_name
end
end
end


# All audit operations during the block are recorded as being
# made by +user+. This is not model specific, the method is a
# convenience wrapper around #Audit.as_user.
def audit_as( user, &block )
Audit.as_user( user, &block )
end

end
end
end
Expand Down
41 changes: 30 additions & 11 deletions lib/acts_as_audited/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,32 @@
class Audit < ActiveRecord::Base
belongs_to :auditable, :polymorphic => true
belongs_to :user, :polymorphic => true
before_create :set_version_number

before_create :set_version_number, :set_audit_user

serialize :changes

cattr_accessor :audited_class_names
self.audited_class_names = Set.new

def self.audited_classes
self.audited_class_names.map(&:constantize)
end


cattr_accessor :audit_as_user
self.audit_as_user = nil

# 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 self.as_user(user, &block)
Thread.current[:acts_as_audited_user] = user

yield

Thread.current[:acts_as_audited_user] = nil
end

# Allows user to be set to either a string or an ActiveRecord object
def user_as_string=(user) #:nodoc:
# reset both either way
Expand All @@ -39,21 +53,21 @@ def user_as_string #:nodoc:
end
alias_method :user_as_model, :user
alias_method :user, :user_as_string

def revision
attributes = self.class.reconstruct_attributes(ancestors).merge({:version => version})
clazz = auditable_type.constantize
returning clazz.find_by_id(auditable_id) || clazz.new do |m|
m.attributes = attributes
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)|
Expand All @@ -69,7 +83,7 @@ def old_attributes
attrs
end
end

def self.reconstruct_attributes(audits)
attributes = {}
result = audits.collect do |audit|
Expand All @@ -78,7 +92,7 @@ def self.reconstruct_attributes(audits)
end
block_given? ? result : attributes
end

private

def set_version_number
Expand All @@ -89,5 +103,10 @@ def set_version_number
}) || 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
Loading

0 comments on commit b624ba0

Please sign in to comment.