lib/audited/auditor.rb in audited-4.6.0 vs lib/audited/auditor.rb in audited-4.7.0
- old
+ new
@@ -31,33 +31,45 @@
# audited except: :password
# end
#
# * +require_comment+ - Ensures that audit_comment is supplied before
# any create, update or destroy operation.
+ # * +max_audits+ - Limits the number of stored audits.
#
+ # * +if+ - Only audit the model when the given function returns true
+ # * +unless+ - Only audit the model when the given function returns false
+ #
+ # class User < ActiveRecord::Base
+ # audited :if => :active?
+ #
+ # def active?
+ # self.status == 'active'
+ # end
+ # end
+ #
def audited(options = {})
# don't allow multiple calls
return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
extend Audited::Auditor::AuditedClassMethods
include Audited::Auditor::AuditedInstanceMethods
- class_attribute :audit_associated_with, instance_writer: false
+ class_attribute :audit_associated_with, instance_writer: false
class_attribute :audited_options, instance_writer: false
attr_accessor :version, :audit_comment
self.audited_options = options
normalize_audited_options
self.audit_associated_with = audited_options[:associated_with]
if audited_options[:comment_required]
- validates_presence_of :audit_comment, if: :auditing_enabled
- before_destroy :require_comment
+ validate :presence_of_audit_comment
+ before_destroy :require_comment if audited_options[:on].include?(:destroy)
end
- has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name
+ has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
Audited.audit_class.audited_class_names << to_s
after_create :audit_create if audited_options[:on].include?(:create)
before_update :audit_update if audited_options[:on].include?(:update)
before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
@@ -99,13 +111,21 @@
# user.name
# user.version
# end
#
def revisions(from_version = 1)
- audits = self.audits.from_version(from_version)
- return [] if audits.empty?
- audits.map(&:revision)
+ return [] unless audits.from_version(from_version).exists?
+
+ all_audits = audits.select([:audited_changes, :version]).to_a
+ targeted_audits = all_audits.select { |audit| audit.version >= from_version }
+
+ previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
+
+ targeted_audits.map do |audit|
+ previous_attributes.merge!(audit.new_attributes)
+ revision_with(previous_attributes.merge!(version: audit.version))
+ end
end
# Get a specific revision specified by the version number, or +:previous+
# Returns nil for versions greater than revisions count
def revision(version)
@@ -123,10 +143,22 @@
# List of attributes that are audited.
def audited_attributes
attributes.except(*non_audited_columns)
end
+ # Combine multiple audits into one.
+ def combine_audits(audits_to_combine)
+ combine_target = audits_to_combine.last
+ combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
+ combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
+
+ transaction do
+ combine_target.save!
+ audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
+ end
+ end
+
protected
def non_audited_columns
self.class.non_audited_columns
end
@@ -206,45 +238,87 @@
end
def write_audit(attrs)
attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
self.audit_comment = nil
- run_callbacks(:audit) { audits.create(attrs) } if auditing_enabled
+
+ if auditing_enabled
+ run_callbacks(:audit) {
+ audit = audits.create(attrs)
+ combine_audits_if_needed if attrs[:action] != 'create'
+ audit
+ }
+ end
end
+ def presence_of_audit_comment
+ if comment_required_state?
+ errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
+ end
+ end
+
+ def comment_required_state?
+ auditing_enabled &&
+ ((audited_options[:on].include?(:create) && self.new_record?) ||
+ (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
+ end
+
+ def combine_audits_if_needed
+ max_audits = audited_options[:max_audits]
+ if max_audits && (extra_count = audits.count - max_audits) > 0
+ audits_to_combine = audits.limit(extra_count + 1)
+ combine_audits(audits_to_combine)
+ end
+ end
+
def require_comment
if auditing_enabled && audit_comment.blank?
- errors.add(:audit_comment, "Comment required before destruction")
+ errors.add(:audit_comment, "Comment can't be blank!")
return false if Rails.version.start_with?('4.')
- throw :abort
+ throw(:abort)
end
end
CALLBACKS.each do |attr_name|
alias_method "#{attr_name}_callback".to_sym, attr_name
end
def auditing_enabled
- self.class.auditing_enabled
+ return run_conditional_check(audited_options[:if]) &&
+ run_conditional_check(audited_options[:unless], matching: false) &&
+ self.class.auditing_enabled
end
+ def run_conditional_check(condition, matching: true)
+ return true if condition.blank?
+
+ return condition.call(self) == matching if condition.respond_to?(:call)
+ return send(condition) == matching if respond_to?(condition.to_sym)
+
+ true
+ end
+
def auditing_enabled=(val)
self.class.auditing_enabled = val
end
+
+ def reconstruct_attributes(audits)
+ attributes = {}
+ audits.each { |audit| attributes.merge!(audit.new_attributes) }
+ attributes
+ end
end # InstanceMethods
module AuditedClassMethods
# Returns an array of columns that are audited. See non_audited_columns
def audited_columns
@audited_columns ||= column_names - non_audited_columns
end
# We have to calculate this here since column_names may not be available when `audited` is called
def non_audited_columns
- @non_audited_columns ||= audited_options[:only].present? ?
- column_names - audited_options[:only] :
- default_ignored_attributes | audited_options[:except]
+ @non_audited_columns ||= calculate_non_audited_columns
end
def non_audited_columns=(columns)
@audited_columns = nil # reset cached audited columns on assignment
@non_audited_columns = columns.map(&:to_s)
@@ -286,19 +360,32 @@
def auditing_enabled=(val)
Audited.store["#{table_name}_auditing_enabled"] = val
end
- protected
def default_ignored_attributes
- [primary_key, inheritance_column] + Audited.ignored_attributes
+ [primary_key, inheritance_column] | Audited.ignored_attributes
end
+ protected
+
def normalize_audited_options
audited_options[:on] = Array.wrap(audited_options[:on])
audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
+ max_audits = audited_options[:max_audits] || Audited.max_audits
+ audited_options[:max_audits] = Integer(max_audits).abs if max_audits
+ end
+
+ def calculate_non_audited_columns
+ if audited_options[:only].present?
+ (column_names | default_ignored_attributes) - audited_options[:only]
+ elsif audited_options[:except].present?
+ default_ignored_attributes | audited_options[:except]
+ else
+ default_ignored_attributes
+ end
end
end
end
end