Created
December 13, 2012 21:29
-
-
Save Crisfole/4280121 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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