Skip to content

Instantly share code, notes, and snippets.

@Crisfole
Created December 13, 2012 21:29
Show Gist options
  • Save Crisfole/4280121 to your computer and use it in GitHub Desktop.
Save Crisfole/4280121 to your computer and use it in GitHub Desktop.
module Audited
extend ActiveSupport::Concern
included do
class_attribute :options
class_attribute :operation_map
self.operation_map = {
:create => 'Created',
:update => 'Updated',
:destroy => 'Destroyed'
}
self.options = {}
end
module ClassMethods
def audit(options={})
children = options[:children] ? options[:children].collect{|i| i.to_s} : []
attr_reader :is_audited
@is_audited = true
self.options[:column_name_map] = options[:column_name_map] || {}
self.options[:diff_columns] = options[:diff_columns] ? options[:diff_columns].collect{|i| i.to_s} : []
self.options[:ignore_columns] = options[:ignore_columns] ? options[:ignore_columns].collect{|i| i.to_s} : []
self.options[:column_callbacks] = options[:column_callbacks] || {}
self.options[:foreign_key_dereferencing] = options[:foreign_key_dereferencing] || {}
self.options[:many_to_many_children] = {}
self.options[:one_to_many_children] = []
reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
sing_name = assoc.name.to_s.singularize
klass = assoc.class_name
if not children.include? klass
next
end
self.options[:many_to_many_children][assoc.name.to_sym] = klass.to_s
ids_method = :"#{sing_name}_ids="
orig_ids_method = :"original_#{ids_method}"
alias_method orig_ids_method, ids_method
define_method ids_method do |new_ids|
old_ids = self.send(:"#{sing_name}_ids")
self.send(:save_association, assoc.name, old_ids, new_ids)
self.send(orig_ids_method, new_ids)
end
end
reflect_on_all_associations(:has_many).each do |assoc|
klass = assoc.class_name
if not children.include? klass
next
end
self.options[:one_to_many_children].push assoc.name.to_s
end
audit_find_many_to_one
attr_accessor :audit_cache
attr_accessor :audit_user
attr_accessor :audit_comment
before_create do |record|
record.audit_cache = record.save_audit_log(:create)
end
before_update { |record| record.save_audit_log(:update) }
before_destroy { |record| record.save_audit_log(:destroy) }
after_create do |record|
record.audit_cache.pk = record.id
record.audit_cache.save
record.audit_cache = nil
end
after_save do |record|
audit_one_to_many_children { |child| child.audit_called_from_parent = false }
end
after_destroy do |record|
audit_one_to_many_children { |child| child.audit_called_from_parent = false }
end
end
def audit_as_one_to_many(options={})
include Audited::OneToManyChild
attr_accessor :audit_called_from_parent
attr_accessor :audit_cache
attr_accessor :audit_user
attr_accessor :audit_comment
self.options[:is_child] = true
self.options[:field_name] = options[:field_name] || nil
self.options[:field_name_callback] = options[:field_name_callback] || nil
self.options[:column_name_map] = options[:column_name_map] || {}
self.options[:diff_columns] = options[:diff_columns] ? options[:diff_columns].collect{|i| i.to_s} : []
self.options[:ignore_columns] = options[:ignore_columns] ? options[:ignore_columns].collect{|i| i.to_s} : []
self.options[:column_callbacks] = options[:column_callbacks] || {}
self.options[:foreign_key_dereferencing] = options[:foreign_key_dereferencing] || {}
self.options[:identity_column] = options[:identity_column] ? options[:identity_column].to_sym : nil
self.options[:columns_on_remove] = options[:columns_on_remove] ? options[:columns_on_remove].collect{|i| i.to_s} : nil
self.options[:columns_on_add] = options[:columns_on_add] ? options[:columns_on_add].collect{|i| i.to_s} : nil
audit_find_many_to_one
before_create do |record|
record.audit_cache = record.save_parent_audit_logs(:create)
end
before_update do |record|
record.save_parent_audit_logs(:update)
end
before_destroy do |record|
record.save_parent_audit_logs(:destroy)
end
after_create do |record|
return if record.audit_called_from_parent
record.audit_cache.pk = record.id
record.audit_cache.save
record.audit_cache = nil
end
end
def audit_find_many_to_one
self.options[:many_to_one_children] = {}
reflect_on_all_associations(:belongs_to).each do |assoc|
klass = assoc.class_name
self.options[:many_to_one_children][assoc.foreign_key.to_sym] = klass.to_s
end
end
def audit_as_many_to_many(options={})
include Audited::ManyToManyChild
self.options[:is_child] = true
self.options[:field_name] = options[:field_name] || nil
self.options[:field_name_callback] = options[:field_name_callback] || nil
self.options[:name_column] = options[:name_column].to_s
end
end
def association_changes
if not @association_changes
@association_changes = {}
end
@association_changes
end
def save_audit_log(action=nil, table_name=nil, pk=nil, details=nil)
action = action.to_sym
if not details
details = self.audit_details(action)
end
if details.length == 0
return
end
audit_log = nil
ActiveRecord::Base.transaction do
audit_log = AuditLog.new
audit_log.person_id = self.audit_user
audit_log.operation = self.class.operation_map[action]
audit_log.table = table_name || self.class.table_name
audit_log.pk = pk || self.id
audit_log.comment = self.audit_comment
audit_log.save
details.each do |hash|
audit_detail = AuditDetail.new
audit_detail.audit_log_id = audit_log.id
audit_detail.attributes = hash
audit_detail.save
end
end
audit_log
end
def save_parent_audit_logs(action)
return if audit_called_from_parent
options[:many_to_one_children].each do |foreign_key, klass|
klass = klass.constantize
if klass.respond_to? :is_audited
audit_cache = save_audit_log(action, klass.table_name, send(foreign_key), [audit_details])
end
end
end
def audit_details(action=nil)
if action.to_sym == :destroy
return []
end
self.audit_detail_changes
end
def audit_fk_value(column, value=false)
klass = options[:many_to_one_children][column.to_sym]
new_column, method = options[:foreign_key_dereferencing][column.to_sym]
if value == false
value = self.send(column)
end
# Don't try lookups for null
if value
value = klass.constantize.find(value).send(method)
end
[new_column, value]
end
def audit_detail_changes(omit_action=false)
output = []
self.changes.each do |column, values|
if options[:ignore_columns].include? column
next
end
if options[:foreign_key_dereferencing][column.to_sym]
_, values[0] = audit_fk_value(column, values[0])
column, values[1] = audit_fk_value(column, values[1])
elsif options[:column_callbacks][column.to_sym]
callback = options[:column_callbacks][column.to_sym]
_, values[0] = callback.call values[0]
column, values[1] = callback.call values[1]
end
field_name = options[:column_name_map][column.to_sym] || column.titleize
hash = {
:field_name => field_name
}
if not omit_action
hash[:action] = 'Changed'
end
if options[:diff_columns].include?(column) and values[0] != nil and values[1] != nil
hash[:json] = {:diff => Differ.diff_by_word(values[1], values[0])}
else
hash[:json] = {:old => values[0], :new => values[1]}
end
output.push hash
end
association_changes.each do |assoc, values|
klass = options[:many_to_many_children][assoc.to_sym]
old_values = values[0] - values[1]
new_values = values[1] - values[0]
old_values.each do |val|
record = klass.constantize.find(val)
output.push({
:field_name => record.audit_field_name,
:action => 'Disassociated',
:json => record.audit_name_hash
})
end
new_values.each do |val|
record = klass.constantize.find(val)
output.push({
:field_name => record.audit_field_name,
:action => 'Associated',
:json => record.audit_name_hash
})
end
end
audit_one_to_many_children do |record|
record.audit_called_from_parent = true
output += record.audit_details(skip_hooks: true)
end
output
end
def audit_one_to_many_children
return unless options[:one_to_many_children]
options[:one_to_many_children].each do |assoc_name|
self.send(assoc_name).each do |record|
yield record
end
end
end
def save_association(association, old_ids, new_ids)
association_changes[association.to_s] = [old_ids, new_ids]
end
module OneToManyChild
def audit_details
if self.marked_for_destruction?
return [self.audit_detail_remove]
end
if not self.changed?
return []
end
if not self.id
return [self.audit_detail_add]
end
if options[:identity_column] and self.changed.include?(options[:identity_column])
return [
self.audit_detail_remove,
self.audit_detail_add
]
end
field_name = self.class.to_s.titleize
if options[:field_name]
field_name = options[:field_name]
elsif options[:field_name_callback]
field_name = self.send(options[:field_name_callback])
end
id_column = options[:identity_column] || :id
id_column_value = self.send(id_column)
id_column_name = id_column
if options[:foreign_key_dereferencing][id_column]
id_column_name, id_column_value = audit_fk_value(id_column)
elsif options[:column_name_map][id_column]
id_column_name = options[:column_name_map][id_column]
end
json = {}
json[id_column_name.to_s.titleize] = id_column_value
json[:changes] = self.audit_detail_changes(true)
[{
:field_name => field_name,
:action => 'Modified',
:json => json
}]
end
def audit_detail_add
field_name = self.class.to_s.titleize
if options[:field_name]
field_name = options[:field_name]
elsif options[:field_name_callback]
field_name = self.send(options[:field_name_callback])
end
{
:field_name => field_name,
:action => 'Added',
:json => self.audit_values(options[:columns_on_add])
}
end
def audit_detail_remove
field_name = self.class.to_s.titleize
if options[:field_name]
field_name = options[:field_name]
elsif options[:field_name_callback]
field_name = self.send(options[:field_name_callback])
end
{
:field_name => field_name,
:action => 'Removed',
:json => self.audit_values(options[:columns_on_remove], true)
}
end
def audit_values(columns_to_filter, original_values=false)
values = attributes
if original_values
values.merge!(self.changed_attributes)
end
options[:foreign_key_dereferencing].each do |key, info|
logger.debug key
logger.debug info
new_key, value = audit_fk_value(key)
logger.debug new_key
logger.debug value
values.delete key.to_s
values[new_key] = value
logger.debug values
end
options[:column_callbacks].each do |key, callback|
new_key, value = callback.call values[key]
values.delete key.to_s
values[new_key] = value
end
if options[:ignore_columns].length
values = values.except(*options[:ignore_columns])
end
if columns_to_filter
values = values.slice(*columns_to_filter)
end
output = {}
values.each do |column, value|
field_name = options[:column_name_map][column.to_sym] || column.titleize
if value == nil
next
end
output[field_name] = value
end
output
end
end
module ManyToManyChild
def audit_field_name
field_name = self.class.to_s.titleize
if options[:field_name]
field_name = options[:field_name]
elsif options[:field_name_callback]
field_name = self.send(options[:field_name_callback])
end
field_name
end
def audit_name_hash
name = options[:name_column].titleize
output = {}
output[name] = self.send(options[:name_column])
output
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment