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