Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for audit_attributes #732

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def audited(options = {})

class_attribute :audit_associated_with, instance_writer: false
class_attribute :audited_options, instance_writer: false
attr_accessor :audit_version, :audit_comment
attr_accessor :audit_version, :audit_comment, :audit_attributes

self.audited_options = options
normalize_audited_options
Expand All @@ -79,6 +79,8 @@ def audited(options = {})
before_destroy :require_comment if audited_options[:on].include?(:destroy)
end

validate :validate_audit_attributes

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

Expand Down Expand Up @@ -332,32 +334,33 @@ def audits_to(version = nil)

def audit_create
write_audit(action: "create", audited_changes: audited_attributes,
comment: audit_comment)
comment: audit_comment, **safe_audit_attributes)
end

def audit_update
unless (changes = audited_changes(exclude_readonly_attrs: true)).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
write_audit(action: "update", audited_changes: changes,
comment: audit_comment)
comment: audit_comment, **safe_audit_attributes)
end
end

def audit_touch
unless (changes = audited_changes(for_touch: true, exclude_readonly_attrs: true)).empty?
write_audit(action: "update", audited_changes: changes,
comment: audit_comment)
comment: audit_comment, **safe_audit_attributes)
end
end

def audit_destroy
unless new_record?
write_audit(action: "destroy", audited_changes: audited_attributes,
comment: audit_comment)
comment: audit_comment, **safe_audit_attributes)
end
end

def write_audit(attrs)
self.audit_comment = nil
self.audit_attributes = nil

if auditing_enabled
attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
Expand All @@ -376,6 +379,18 @@ def presence_of_audit_comment
end
end

def validate_audit_attributes
return unless audit_attributes.present?
audit_columns = Audited.audit_class.column_names.map(&:to_sym)
if !audit_attributes.is_a?(Hash) || (audit_attributes.keys - audit_columns).any?
errors.add(:audit_attributes, "must be a hash including only the keys of the audit class (#{audit_columns.join(", ")})")
end
end

def safe_audit_attributes
audit_attributes.is_a?(Hash) ? audit_attributes : {}
end

def comment_required_state?
auditing_enabled &&
audited_changes.present? &&
Expand Down
48 changes: 45 additions & 3 deletions spec/audited/auditor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ class CallbacksSpecified < ::ActiveRecord::Base
end

describe "on create" do
let(:user) { create_user status: :reliable, audit_comment: "Create" }
let(:user) { create_user status: :reliable, audit_comment: "Create", audit_attributes: {custom_attribute: "Create Value"} }

it "should change the audit count" do
expect {
Expand Down Expand Up @@ -370,6 +370,10 @@ class CallbacksSpecified < ::ActiveRecord::Base
expect(user.audits.first.comment).to eq("Create")
end

it "should set the audit_attributes" do
expect(user.audits.first.custom_attribute).to eq("Create Value")
end

it "should not audit an attribute which is excepted if specified on create or destroy" do
on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: "Bart")
expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any? { |col| ["name"].include? col }).to eq(false)
Expand All @@ -390,7 +394,7 @@ class CallbacksSpecified < ::ActiveRecord::Base

describe "on update" do
before do
@user = create_user(name: "Brandon", status: :active, audit_comment: "Update")
@user = create_user(name: "Brandon", status: :active, audit_comment: "Update", audit_attributes: {custom_attribute: "Update Value"})
end

it "should save an audit" do
Expand Down Expand Up @@ -423,6 +427,10 @@ class CallbacksSpecified < ::ActiveRecord::Base
expect(@user.audits.last.comment).to eq("Update")
end

it "should set the audit_attributes" do
expect(@user.audits.first.custom_attribute).to eq("Update Value")
end

it "should not save an audit if only specified on create/destroy" do
on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create(name: "Bart")
expect {
Expand Down Expand Up @@ -1135,7 +1143,7 @@ def stub_global_max_audits(max_audits)
end

describe "on update" do
let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create", audit_attributes: {custom_attribute: "Create Value"}) }
let(:on_create_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
let(:on_destroy_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }

Expand Down Expand Up @@ -1270,4 +1278,38 @@ def stub_global_max_audits(max_audits)
}.to_not change(Audited::Audit, :count)
end
end

describe "Setting audit_attributes" do
let(:user) { create_user status: :reliable, audit_comment: "Create", audit_attributes: {custom_attribute: "Create Value", comment: "Overrided Comment"} }

it "should change the audit count" do
expect {
user
}.to change(Models::ActiveRecord::User, :count).by(1)
end

it "should set custom attributes" do
expect(user.audits.first.custom_attribute).to eq("Create Value")
end

it "should override set attributes (comment)" do
expect(user.audits.first.comment).to eq("Overrided Comment")
end

it "doesn't affect unset attributes (status)" do
expect(user.audits.first.audited_changes["status"]).to eq(1)
end

it "validates that audit_attributes is a hash" do
expect {
Models::ActiveRecord::User.create!(audit_attributes: 4)
}.to raise_error(ActiveRecord::RecordInvalid).with_message(/Audit attributes must be a hash including only the keys of the audit class/)
end

it "validates that audit_attributes is a hash with only valid keys" do
expect {
Models::ActiveRecord::User.create!(audit_attributes: {custom_attribute: "Create Value", comment: "Overrided Comment", invalid_key: "Invalid Key"})
}.to raise_error(ActiveRecord::RecordInvalid).with_message(/Audit attributes must be a hash including only the keys of the audit class/)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddColumnCustomAttributeToAudits < ActiveRecord::Migration[5.0]
def change
add_column :audits, :custom_attribute, :string
end
end
1 change: 1 addition & 0 deletions spec/support/active_record/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
t.column :remote_address, :string
t.column :request_uuid, :string
t.column :created_at, :datetime
t.column :custom_attribute, :string
end

add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
Expand Down